diff --git a/.eslintignore b/.eslintignore index 1806ae7c09..6676467368 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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/ diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f1b56462f2..15d8c36a8a 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02b8d1947..aa94465faf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' diff --git a/.rubocop.yml b/.rubocop.yml index 1e682c1045..f389e688ca 100644 --- a/.rubocop.yml +++ b/.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 diff --git a/.template-lintrc.js b/.template-lintrc.js index 5731778fc0..d7b4a104eb 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -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 } }; diff --git a/.tx/config b/.tx/config index 7167d96bc5..9163537384 100644 --- a/.tx/config +++ b/.tx/config @@ -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..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..yml source_file = plugins/discourse-local-dates/config/locales/client.en.yml diff --git a/Dangerfile b/Dangerfile index 667918b36a..cfa678978a 100644 --- a/Dangerfile +++ b/Dangerfile @@ -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 = [] diff --git a/Gemfile b/Gemfile index ced7f0376e..c440694fff 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index 5d4ea41328..e29ba65db6 100644 --- a/Gemfile.lock +++ b/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 diff --git a/README.md b/README.md index 323a21e73b..ddf32ba126 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/assets/javascripts/activate-account.js b/app/assets/javascripts/activate-account.js new file mode 100644 index 0000000000..b637b27763 --- /dev/null +++ b/app/assets/javascripts/activate-account.js @@ -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); +})(); diff --git a/app/assets/javascripts/activate-account.js.no-module.es6 b/app/assets/javascripts/activate-account.js.no-module.es6 deleted file mode 100644 index 8d5691d0d8..0000000000 --- a/app/assets/javascripts/activate-account.js.no-module.es6 +++ /dev/null @@ -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); -})(); diff --git a/app/assets/javascripts/admin/adapters/api-key.js.es6 b/app/assets/javascripts/admin/adapters/api-key.js similarity index 100% rename from app/assets/javascripts/admin/adapters/api-key.js.es6 rename to app/assets/javascripts/admin/adapters/api-key.js diff --git a/app/assets/javascripts/admin/adapters/build-plugin.js.es6 b/app/assets/javascripts/admin/adapters/build-plugin.js similarity index 100% rename from app/assets/javascripts/admin/adapters/build-plugin.js.es6 rename to app/assets/javascripts/admin/adapters/build-plugin.js diff --git a/app/assets/javascripts/admin/adapters/customization-base.js.es6 b/app/assets/javascripts/admin/adapters/customization-base.js similarity index 100% rename from app/assets/javascripts/admin/adapters/customization-base.js.es6 rename to app/assets/javascripts/admin/adapters/customization-base.js diff --git a/app/assets/javascripts/admin/adapters/email-style.js.es6 b/app/assets/javascripts/admin/adapters/email-style.js similarity index 100% rename from app/assets/javascripts/admin/adapters/email-style.js.es6 rename to app/assets/javascripts/admin/adapters/email-style.js diff --git a/app/assets/javascripts/admin/adapters/embedding.js.es6 b/app/assets/javascripts/admin/adapters/embedding.js similarity index 100% rename from app/assets/javascripts/admin/adapters/embedding.js.es6 rename to app/assets/javascripts/admin/adapters/embedding.js diff --git a/app/assets/javascripts/admin/adapters/site-text.js.es6 b/app/assets/javascripts/admin/adapters/site-text.js similarity index 100% rename from app/assets/javascripts/admin/adapters/site-text.js.es6 rename to app/assets/javascripts/admin/adapters/site-text.js diff --git a/app/assets/javascripts/admin/adapters/staff-action-log.js.es6 b/app/assets/javascripts/admin/adapters/staff-action-log.js similarity index 100% rename from app/assets/javascripts/admin/adapters/staff-action-log.js.es6 rename to app/assets/javascripts/admin/adapters/staff-action-log.js diff --git a/app/assets/javascripts/admin/adapters/tag-group.js.es6 b/app/assets/javascripts/admin/adapters/tag-group.js similarity index 100% rename from app/assets/javascripts/admin/adapters/tag-group.js.es6 rename to app/assets/javascripts/admin/adapters/tag-group.js diff --git a/app/assets/javascripts/admin/adapters/theme.js.es6 b/app/assets/javascripts/admin/adapters/theme.js similarity index 100% rename from app/assets/javascripts/admin/adapters/theme.js.es6 rename to app/assets/javascripts/admin/adapters/theme.js diff --git a/app/assets/javascripts/admin/adapters/user-field.js.es6 b/app/assets/javascripts/admin/adapters/user-field.js similarity index 100% rename from app/assets/javascripts/admin/adapters/user-field.js.es6 rename to app/assets/javascripts/admin/adapters/user-field.js diff --git a/app/assets/javascripts/admin/adapters/web-hook-event.js.es6 b/app/assets/javascripts/admin/adapters/web-hook-event.js similarity index 100% rename from app/assets/javascripts/admin/adapters/web-hook-event.js.es6 rename to app/assets/javascripts/admin/adapters/web-hook-event.js diff --git a/app/assets/javascripts/admin/adapters/web-hook.js.es6 b/app/assets/javascripts/admin/adapters/web-hook.js similarity index 100% rename from app/assets/javascripts/admin/adapters/web-hook.js.es6 rename to app/assets/javascripts/admin/adapters/web-hook.js diff --git a/app/assets/javascripts/admin/components/ace-editor.js b/app/assets/javascripts/admin/components/ace-editor.js new file mode 100644 index 0000000000..123193c0c0 --- /dev/null +++ b/app/assets/javascripts/admin/components/ace-editor.js @@ -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(); + } + } + } +}); diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 deleted file mode 100644 index 4fc881dae4..0000000000 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ /dev/null @@ -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(); - } - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/components/admin-backups-logs.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-backups-logs.js.es6 rename to app/assets/javascripts/admin/components/admin-backups-logs.js diff --git a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 b/app/assets/javascripts/admin/components/admin-directory-toggle.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 rename to app/assets/javascripts/admin/components/admin-directory-toggle.js diff --git a/app/assets/javascripts/admin/components/admin-editable-field.js.es6 b/app/assets/javascripts/admin/components/admin-editable-field.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-editable-field.js.es6 rename to app/assets/javascripts/admin/components/admin-editable-field.js diff --git a/app/assets/javascripts/admin/components/admin-form-row.js.es6 b/app/assets/javascripts/admin/components/admin-form-row.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-form-row.js.es6 rename to app/assets/javascripts/admin/components/admin-form-row.js diff --git a/app/assets/javascripts/admin/components/admin-graph.js.es6 b/app/assets/javascripts/admin/components/admin-graph.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-graph.js.es6 rename to app/assets/javascripts/admin/components/admin-graph.js diff --git a/app/assets/javascripts/admin/components/admin-nav.js.es6 b/app/assets/javascripts/admin/components/admin-nav.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-nav.js.es6 rename to app/assets/javascripts/admin/components/admin-nav.js diff --git a/app/assets/javascripts/admin/components/admin-report-chart.js b/app/assets/javascripts/admin/components/admin-report-chart.js new file mode 100644 index 0000000000..7af8ce0474 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-chart.js @@ -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; + } + } +}); diff --git a/app/assets/javascripts/admin/components/admin-report-chart.js.es6 b/app/assets/javascripts/admin/components/admin-report-chart.js.es6 deleted file mode 100644 index 5351a11952..0000000000 --- a/app/assets/javascripts/admin/components/admin-report-chart.js.es6 +++ /dev/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; - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-report-counters.js.es6 b/app/assets/javascripts/admin/components/admin-report-counters.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-counters.js.es6 rename to app/assets/javascripts/admin/components/admin-report-counters.js diff --git a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-counts.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-counts.js.es6 rename to app/assets/javascripts/admin/components/admin-report-counts.js diff --git a/app/assets/javascripts/admin/components/admin-report-inline-table.js.es6 b/app/assets/javascripts/admin/components/admin-report-inline-table.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-inline-table.js.es6 rename to app/assets/javascripts/admin/components/admin-report-inline-table.js diff --git a/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-per-day-counts.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 rename to app/assets/javascripts/admin/components/admin-report-per-day-counts.js diff --git a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js new file mode 100644 index 0000000000..8980c93077 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js @@ -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; + } + } +}); diff --git a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 deleted file mode 100644 index 911d858ba9..0000000000 --- a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 +++ /dev/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; - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-report-storage-stats.js.es6 b/app/assets/javascripts/admin/components/admin-report-storage-stats.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-storage-stats.js.es6 rename to app/assets/javascripts/admin/components/admin-report-storage-stats.js diff --git a/app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-cell.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-table-cell.js.es6 rename to app/assets/javascripts/admin/components/admin-report-table-cell.js diff --git a/app/assets/javascripts/admin/components/admin-report-table-header.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-header.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-table-header.js.es6 rename to app/assets/javascripts/admin/components/admin-report-table-header.js diff --git a/app/assets/javascripts/admin/components/admin-report-table-row.js.es6 b/app/assets/javascripts/admin/components/admin-report-table-row.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-table-row.js.es6 rename to app/assets/javascripts/admin/components/admin-report-table-row.js diff --git a/app/assets/javascripts/admin/components/admin-report-table.js.es6 b/app/assets/javascripts/admin/components/admin-report-table.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-table.js.es6 rename to app/assets/javascripts/admin/components/admin-report-table.js diff --git a/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 rename to app/assets/javascripts/admin/components/admin-report-trust-level-counts.js diff --git a/app/assets/javascripts/admin/components/admin-report.js b/app/assets/javascripts/admin/components/admin-report.js new file mode 100644 index 0000000000..178bef98f2 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report.js @@ -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); + } +}); diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 deleted file mode 100644 index a385b71730..0000000000 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ /dev/null @@ -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); - } -}); diff --git a/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 b/app/assets/javascripts/admin/components/admin-theme-editor.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-theme-editor.js.es6 rename to app/assets/javascripts/admin/components/admin-theme-editor.js diff --git a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 b/app/assets/javascripts/admin/components/admin-user-field-item.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-user-field-item.js.es6 rename to app/assets/javascripts/admin/components/admin-user-field-item.js diff --git a/app/assets/javascripts/admin/components/admin-watched-word.js.es6 b/app/assets/javascripts/admin/components/admin-watched-word.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-watched-word.js.es6 rename to app/assets/javascripts/admin/components/admin-watched-word.js diff --git a/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 rename to app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js diff --git a/app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-event.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 rename to app/assets/javascripts/admin/components/admin-web-hook-event.js diff --git a/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-status.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 rename to app/assets/javascripts/admin/components/admin-web-hook-status.js diff --git a/app/assets/javascripts/admin/components/admin-wrapper.js.es6 b/app/assets/javascripts/admin/components/admin-wrapper.js similarity index 100% rename from app/assets/javascripts/admin/components/admin-wrapper.js.es6 rename to app/assets/javascripts/admin/components/admin-wrapper.js diff --git a/app/assets/javascripts/admin/components/cancel-link.js.es6 b/app/assets/javascripts/admin/components/cancel-link.js similarity index 100% rename from app/assets/javascripts/admin/components/cancel-link.js.es6 rename to app/assets/javascripts/admin/components/cancel-link.js diff --git a/app/assets/javascripts/admin/components/color-input.js b/app/assets/javascripts/admin/components/color-input.js new file mode 100644 index 0000000000..d1b6102c61 --- /dev/null +++ b/app/assets/javascripts/admin/components/color-input.js @@ -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()); + } +}); diff --git a/app/assets/javascripts/admin/components/color-input.js.es6 b/app/assets/javascripts/admin/components/color-input.js.es6 deleted file mode 100644 index 8648506bf9..0000000000 --- a/app/assets/javascripts/admin/components/color-input.js.es6 +++ /dev/null @@ -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(); - }); - } -}); diff --git a/app/assets/javascripts/admin/components/email-styles-editor.js.es6 b/app/assets/javascripts/admin/components/email-styles-editor.js similarity index 100% rename from app/assets/javascripts/admin/components/email-styles-editor.js.es6 rename to app/assets/javascripts/admin/components/email-styles-editor.js diff --git a/app/assets/javascripts/admin/components/embeddable-host.js.es6 b/app/assets/javascripts/admin/components/embeddable-host.js similarity index 100% rename from app/assets/javascripts/admin/components/embeddable-host.js.es6 rename to app/assets/javascripts/admin/components/embeddable-host.js diff --git a/app/assets/javascripts/admin/components/embedding-setting.js.es6 b/app/assets/javascripts/admin/components/embedding-setting.js similarity index 100% rename from app/assets/javascripts/admin/components/embedding-setting.js.es6 rename to app/assets/javascripts/admin/components/embedding-setting.js diff --git a/app/assets/javascripts/admin/components/flag-user-lists.js.es6 b/app/assets/javascripts/admin/components/flag-user-lists.js similarity index 100% rename from app/assets/javascripts/admin/components/flag-user-lists.js.es6 rename to app/assets/javascripts/admin/components/flag-user-lists.js diff --git a/app/assets/javascripts/admin/components/highlighted-code.js.es6 b/app/assets/javascripts/admin/components/highlighted-code.js similarity index 100% rename from app/assets/javascripts/admin/components/highlighted-code.js.es6 rename to app/assets/javascripts/admin/components/highlighted-code.js diff --git a/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 b/app/assets/javascripts/admin/components/inline-edit-checkbox.js similarity index 100% rename from app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 rename to app/assets/javascripts/admin/components/inline-edit-checkbox.js diff --git a/app/assets/javascripts/admin/components/install-theme-item.js.es6 b/app/assets/javascripts/admin/components/install-theme-item.js similarity index 100% rename from app/assets/javascripts/admin/components/install-theme-item.js.es6 rename to app/assets/javascripts/admin/components/install-theme-item.js diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js similarity index 100% rename from app/assets/javascripts/admin/components/ip-lookup.js.es6 rename to app/assets/javascripts/admin/components/ip-lookup.js diff --git a/app/assets/javascripts/admin/components/moderation-history-item.js.es6 b/app/assets/javascripts/admin/components/moderation-history-item.js similarity index 100% rename from app/assets/javascripts/admin/components/moderation-history-item.js.es6 rename to app/assets/javascripts/admin/components/moderation-history-item.js diff --git a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 b/app/assets/javascripts/admin/components/penalty-post-action.js similarity index 100% rename from app/assets/javascripts/admin/components/penalty-post-action.js.es6 rename to app/assets/javascripts/admin/components/penalty-post-action.js diff --git a/app/assets/javascripts/admin/components/permalink-form.js.es6 b/app/assets/javascripts/admin/components/permalink-form.js similarity index 100% rename from app/assets/javascripts/admin/components/permalink-form.js.es6 rename to app/assets/javascripts/admin/components/permalink-form.js diff --git a/app/assets/javascripts/admin/components/report-filters/bool.js b/app/assets/javascripts/admin/components/report-filters/bool.js new file mode 100644 index 0000000000..777e15cfe4 --- /dev/null +++ b/app/assets/javascripts/admin/components/report-filters/bool.js @@ -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); + } +}); diff --git a/app/assets/javascripts/admin/components/report-filters/category.js b/app/assets/javascripts/admin/components/report-filters/category.js new file mode 100644 index 0000000000..5a5ab52098 --- /dev/null +++ b/app/assets/javascripts/admin/components/report-filters/category.js @@ -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); + } +}); diff --git a/app/assets/javascripts/admin/components/report-filters/category.js.es6 b/app/assets/javascripts/admin/components/report-filters/category.js.es6 deleted file mode 100644 index fab7753da4..0000000000 --- a/app/assets/javascripts/admin/components/report-filters/category.js.es6 +++ /dev/null @@ -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); - } - } -}); diff --git a/app/assets/javascripts/admin/components/report-filters/file-extension.js.es6 b/app/assets/javascripts/admin/components/report-filters/file-extension.js.es6 deleted file mode 100644 index d8eb1b5b06..0000000000 --- a/app/assets/javascripts/admin/components/report-filters/file-extension.js.es6 +++ /dev/null @@ -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" -}); diff --git a/app/assets/javascripts/admin/components/report-filters/filter.js b/app/assets/javascripts/admin/components/report-filters/filter.js new file mode 100644 index 0000000000..0c4e006346 --- /dev/null +++ b/app/assets/javascripts/admin/components/report-filters/filter.js @@ -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); + } +}); diff --git a/app/assets/javascripts/admin/components/report-filters/filter.js.es6 b/app/assets/javascripts/admin/components/report-filters/filter.js.es6 deleted file mode 100644 index f61b2d496a..0000000000 --- a/app/assets/javascripts/admin/components/report-filters/filter.js.es6 +++ /dev/null @@ -1,8 +0,0 @@ -import Component from "@ember/component"; -export default Component.extend({ - actions: { - onChange(value) { - this.applyFilter(this.get("filter.id"), value); - } - } -}); diff --git a/app/assets/javascripts/admin/components/report-filters/group.js b/app/assets/javascripts/admin/components/report-filters/group.js new file mode 100644 index 0000000000..e841800030 --- /dev/null +++ b/app/assets/javascripts/admin/components/report-filters/group.js @@ -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; + } +}); diff --git a/app/assets/javascripts/admin/components/report-filters/group.js.es6 b/app/assets/javascripts/admin/components/report-filters/group.js.es6 deleted file mode 100644 index a6511e75f6..0000000000 --- a/app/assets/javascripts/admin/components/report-filters/group.js.es6 +++ /dev/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; - } -}); diff --git a/app/assets/javascripts/admin/components/report-filters/list.js b/app/assets/javascripts/admin/components/report-filters/list.js new file mode 100644 index 0000000000..654269ecd0 --- /dev/null +++ b/app/assets/javascripts/admin/components/report-filters/list.js @@ -0,0 +1,3 @@ +import FilterComponent from "admin/components/report-filters/filter"; + +export default FilterComponent.extend(); diff --git a/app/assets/javascripts/admin/components/resumable-upload.js b/app/assets/javascripts/admin/components/resumable-upload.js new file mode 100644 index 0000000000..7425f174fe --- /dev/null +++ b/app/assets/javascripts/admin/components/resumable-upload.js @@ -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(); + } +}); diff --git a/app/assets/javascripts/admin/components/resumable-upload.js.es6 b/app/assets/javascripts/admin/components/resumable-upload.js.es6 deleted file mode 100644 index 830e560252..0000000000 --- a/app/assets/javascripts/admin/components/resumable-upload.js.es6 +++ /dev/null @@ -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(); - } -}); diff --git a/app/assets/javascripts/admin/components/save-controls.js.es6 b/app/assets/javascripts/admin/components/save-controls.js.es6 deleted file mode 100644 index 9da4e49fe2..0000000000 --- a/app/assets/javascripts/admin/components/save-controls.js.es6 +++ /dev/null @@ -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"; - } -}); diff --git a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 b/app/assets/javascripts/admin/components/screened-ip-address-form.js similarity index 100% rename from app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 rename to app/assets/javascripts/admin/components/screened-ip-address-form.js diff --git a/app/assets/javascripts/admin/components/secret-value-list.js.es6 b/app/assets/javascripts/admin/components/secret-value-list.js similarity index 100% rename from app/assets/javascripts/admin/components/secret-value-list.js.es6 rename to app/assets/javascripts/admin/components/secret-value-list.js diff --git a/app/assets/javascripts/admin/components/silence-details.js.es6 b/app/assets/javascripts/admin/components/silence-details.js similarity index 100% rename from app/assets/javascripts/admin/components/silence-details.js.es6 rename to app/assets/javascripts/admin/components/silence-details.js diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js similarity index 100% rename from app/assets/javascripts/admin/components/site-setting.js.es6 rename to app/assets/javascripts/admin/components/site-setting.js diff --git a/app/assets/javascripts/admin/components/site-settings-image-uploader.js.es6 b/app/assets/javascripts/admin/components/site-settings-image-uploader.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings-image-uploader.js.es6 rename to app/assets/javascripts/admin/components/site-settings-image-uploader.js diff --git a/app/assets/javascripts/admin/components/site-settings/bool.js.es6 b/app/assets/javascripts/admin/components/site-settings/bool.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings/bool.js.es6 rename to app/assets/javascripts/admin/components/site-settings/bool.js diff --git a/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/category-list.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings/category-list.js.es6 rename to app/assets/javascripts/admin/components/site-settings/category-list.js diff --git a/app/assets/javascripts/admin/components/site-settings/color.js b/app/assets/javascripts/admin/components/site-settings/color.js new file mode 100644 index 0000000000..5b7e40e34c --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/color.js @@ -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); + } +}); diff --git a/app/assets/javascripts/admin/components/site-settings/compact-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/compact-list.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings/compact-list.js.es6 rename to app/assets/javascripts/admin/components/site-settings/compact-list.js diff --git a/app/assets/javascripts/admin/components/site-settings/group-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/group-list.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings/group-list.js.es6 rename to app/assets/javascripts/admin/components/site-settings/group-list.js diff --git a/app/assets/javascripts/admin/components/site-settings/tag-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/tag-list.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings/tag-list.js.es6 rename to app/assets/javascripts/admin/components/site-settings/tag-list.js diff --git a/app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js similarity index 100% rename from app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js.es6 rename to app/assets/javascripts/admin/components/site-settings/uploaded-image-list.js diff --git a/app/assets/javascripts/admin/components/site-text-summary.js b/app/assets/javascripts/admin/components/site-text-summary.js new file mode 100644 index 0000000000..23ee2d8aa6 --- /dev/null +++ b/app/assets/javascripts/admin/components/site-text-summary.js @@ -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; + } +}); diff --git a/app/assets/javascripts/admin/components/site-text-summary.js.es6 b/app/assets/javascripts/admin/components/site-text-summary.js.es6 deleted file mode 100644 index 11c6bc45eb..0000000000 --- a/app/assets/javascripts/admin/components/site-text-summary.js.es6 +++ /dev/null @@ -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; - } -}); diff --git a/app/assets/javascripts/admin/components/staff-actions.js.es6 b/app/assets/javascripts/admin/components/staff-actions.js similarity index 100% rename from app/assets/javascripts/admin/components/staff-actions.js.es6 rename to app/assets/javascripts/admin/components/staff-actions.js diff --git a/app/assets/javascripts/admin/components/suspension-details.js.es6 b/app/assets/javascripts/admin/components/suspension-details.js similarity index 100% rename from app/assets/javascripts/admin/components/suspension-details.js.es6 rename to app/assets/javascripts/admin/components/suspension-details.js diff --git a/app/assets/javascripts/admin/components/tags-uploader.js.es6 b/app/assets/javascripts/admin/components/tags-uploader.js similarity index 100% rename from app/assets/javascripts/admin/components/tags-uploader.js.es6 rename to app/assets/javascripts/admin/components/tags-uploader.js diff --git a/app/assets/javascripts/admin/components/theme-setting-editor.js.es6 b/app/assets/javascripts/admin/components/theme-setting-editor.js similarity index 100% rename from app/assets/javascripts/admin/components/theme-setting-editor.js.es6 rename to app/assets/javascripts/admin/components/theme-setting-editor.js diff --git a/app/assets/javascripts/admin/components/theme-setting-relatives-selector.js.es6 b/app/assets/javascripts/admin/components/theme-setting-relatives-selector.js similarity index 100% rename from app/assets/javascripts/admin/components/theme-setting-relatives-selector.js.es6 rename to app/assets/javascripts/admin/components/theme-setting-relatives-selector.js diff --git a/app/assets/javascripts/admin/components/theme-translation.js.es6 b/app/assets/javascripts/admin/components/theme-translation.js similarity index 100% rename from app/assets/javascripts/admin/components/theme-translation.js.es6 rename to app/assets/javascripts/admin/components/theme-translation.js diff --git a/app/assets/javascripts/admin/components/themes-list-item.js.es6 b/app/assets/javascripts/admin/components/themes-list-item.js similarity index 100% rename from app/assets/javascripts/admin/components/themes-list-item.js.es6 rename to app/assets/javascripts/admin/components/themes-list-item.js diff --git a/app/assets/javascripts/admin/components/themes-list.js.es6 b/app/assets/javascripts/admin/components/themes-list.js similarity index 100% rename from app/assets/javascripts/admin/components/themes-list.js.es6 rename to app/assets/javascripts/admin/components/themes-list.js diff --git a/app/assets/javascripts/admin/components/value-list.js.es6 b/app/assets/javascripts/admin/components/value-list.js similarity index 100% rename from app/assets/javascripts/admin/components/value-list.js.es6 rename to app/assets/javascripts/admin/components/value-list.js diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js similarity index 100% rename from app/assets/javascripts/admin/components/watched-word-form.js.es6 rename to app/assets/javascripts/admin/components/watched-word-form.js diff --git a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 b/app/assets/javascripts/admin/components/watched-word-uploader.js similarity index 100% rename from app/assets/javascripts/admin/components/watched-word-uploader.js.es6 rename to app/assets/javascripts/admin/components/watched-word-uploader.js diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys-index.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-api-keys-index.js.es6 rename to app/assets/javascripts/admin/controllers/admin-api-keys-index.js diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-new.js b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js new file mode 100644 index 0000000000..9384e79557 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js @@ -0,0 +1,40 @@ +import { isBlank } from "@ember/utils"; +import Controller from "@ember/controller"; +import discourseComputed from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default Controller.extend({ + userModes: [ + { id: "all", name: I18n.t("admin.api.all_users") }, + { id: "single", name: I18n.t("admin.api.single_user") } + ], + + @discourseComputed("userMode") + showUserSelector(mode) { + return mode === "single"; + }, + + @discourseComputed("model.description", "model.username", "userMode") + saveDisabled(description, username, userMode) { + if (isBlank(description)) return true; + if (userMode === "single" && isBlank(username)) return true; + return false; + }, + + actions: { + changeUserMode(value) { + if (value === "all") { + this.model.set("username", null); + } + this.set("userMode", value); + }, + + save() { + this.model.save().catch(popupAjaxError); + }, + + continue() { + this.transitionToRoute("adminApiKeys.show", this.model.id); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-new.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js.es6 deleted file mode 100644 index 42391beaf8..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-api-keys-new.js.es6 +++ /dev/null @@ -1,39 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Controller.extend({ - userModes: [ - { id: "all", name: I18n.t("admin.api.all_users") }, - { id: "single", name: I18n.t("admin.api.single_user") } - ], - - @discourseComputed("userMode") - showUserSelector(mode) { - return mode === "single"; - }, - - @discourseComputed("model.description", "model.username", "userMode") - saveDisabled(description, username, userMode) { - if (Ember.isBlank(description)) return true; - if (userMode === "single" && Ember.isBlank(username)) return true; - return false; - }, - - actions: { - changeUserMode(value) { - if (value === "all") { - this.model.set("username", null); - } - this.set("userMode", value); - }, - - save() { - this.model.save().catch(popupAjaxError); - }, - - continue() { - this.transitionToRoute("adminApiKeys.show", this.model.id); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys-show.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-api-keys-show.js.es6 rename to app/assets/javascripts/admin/controllers/admin-api-keys-show.js diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 rename to app/assets/javascripts/admin/controllers/admin-api-keys.js diff --git a/app/assets/javascripts/admin/controllers/admin-backups-index.js b/app/assets/javascripts/admin/controllers/admin-backups-index.js new file mode 100644 index 0000000000..45f25dd617 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js @@ -0,0 +1,58 @@ +import { alias, equal } from "@ember/object/computed"; +import Controller, { inject as controller } from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; +import discourseComputed from "discourse-common/utils/decorators"; +import { setting, i18n } from "discourse/lib/computed"; + +export default Controller.extend({ + adminBackups: controller(), + status: alias("adminBackups.model"), + uploadLabel: i18n("admin.backups.upload.label"), + backupLocation: setting("backup_location"), + localBackupStorage: equal("backupLocation", "local"), + + @discourseComputed("status.allowRestore", "status.isOperationRunning") + restoreTitle(allowRestore, isOperationRunning) { + if (!allowRestore) { + return "admin.backups.operations.restore.is_disabled"; + } else if (isOperationRunning) { + return "admin.backups.operations.is_running"; + } else { + return "admin.backups.operations.restore.title"; + } + }, + + actions: { + toggleReadOnlyMode() { + if (!this.site.get("isReadOnly")) { + bootbox.confirm( + I18n.t("admin.backups.read_only.enable.confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + this.set("currentUser.hideReadOnlyAlert", true); + this._toggleReadOnlyMode(true); + } + } + ); + } else { + this._toggleReadOnlyMode(false); + } + }, + + download(backup) { + const link = backup.get("filename"); + ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() => + bootbox.alert(I18n.t("admin.backups.operations.download.alert")) + ); + } + }, + + _toggleReadOnlyMode(enable) { + ajax("/admin/backups/readonly", { + type: "PUT", + data: { enable } + }).then(() => this.site.set("isReadOnly", enable)); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 deleted file mode 100644 index 01103105e8..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 +++ /dev/null @@ -1,59 +0,0 @@ -import { alias, equal } from "@ember/object/computed"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import discourseComputed from "discourse-common/utils/decorators"; -import { setting, i18n } from "discourse/lib/computed"; - -export default Controller.extend({ - adminBackups: inject(), - status: alias("adminBackups.model"), - uploadLabel: i18n("admin.backups.upload.label"), - backupLocation: setting("backup_location"), - localBackupStorage: equal("backupLocation", "local"), - - @discourseComputed("status.allowRestore", "status.isOperationRunning") - restoreTitle(allowRestore, isOperationRunning) { - if (!allowRestore) { - return "admin.backups.operations.restore.is_disabled"; - } else if (isOperationRunning) { - return "admin.backups.operations.is_running"; - } else { - return "admin.backups.operations.restore.title"; - } - }, - - actions: { - toggleReadOnlyMode() { - if (!this.site.get("isReadOnly")) { - bootbox.confirm( - I18n.t("admin.backups.read_only.enable.confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - this.set("currentUser.hideReadOnlyAlert", true); - this._toggleReadOnlyMode(true); - } - } - ); - } else { - this._toggleReadOnlyMode(false); - } - }, - - download(backup) { - const link = backup.get("filename"); - ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() => - bootbox.alert(I18n.t("admin.backups.operations.download.alert")) - ); - } - }, - - _toggleReadOnlyMode(enable) { - ajax("/admin/backups/readonly", { - type: "PUT", - data: { enable } - }).then(() => this.site.set("isReadOnly", enable)); - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-backups-logs.js b/app/assets/javascripts/admin/controllers/admin-backups-logs.js new file mode 100644 index 0000000000..69a81f9b67 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-backups-logs.js @@ -0,0 +1,13 @@ +import { alias } from "@ember/object/computed"; +import Controller, { inject as controller } from "@ember/controller"; + +export default Controller.extend({ + adminBackups: controller(), + status: alias("adminBackups.model"), + + init() { + this._super(...arguments); + + this.logs = []; + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups-logs.js.es6 deleted file mode 100644 index a32e216255..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-backups-logs.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import { alias } from "@ember/object/computed"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -export default Controller.extend({ - adminBackups: inject(), - status: alias("adminBackups.model"), - - init() { - this._super(...arguments); - - this.logs = []; - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-backups.js.es6 rename to app/assets/javascripts/admin/controllers/admin-backups.js diff --git a/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-award.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 rename to app/assets/javascripts/admin/controllers/admin-badges-award.js diff --git a/app/assets/javascripts/admin/controllers/admin-badges-show.js b/app/assets/javascripts/admin/controllers/admin-badges-show.js new file mode 100644 index 0000000000..a860d2f992 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-badges-show.js @@ -0,0 +1,166 @@ +import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import { reads } from "@ember/object/computed"; +import Controller, { inject as controller } from "@ember/controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; +import { propertyNotEqual } from "discourse/lib/computed"; +import { run } from "@ember/runloop"; + +export default Controller.extend(bufferedProperty("model"), { + adminBadges: controller(), + saving: false, + savingStatus: "", + badgeTypes: reads("adminBadges.badgeTypes"), + badgeGroupings: reads("adminBadges.badgeGroupings"), + badgeTriggers: reads("adminBadges.badgeTriggers"), + protectedSystemFields: reads("adminBadges.protectedSystemFields"), + readOnly: reads("buffered.system"), + showDisplayName: propertyNotEqual("name", "displayName"), + + init() { + this._super(...arguments); + + // this is needed because the model doesnt have default values + // and as we are using a bufferedProperty it's not accessible + // in any other way + run.next(() => { + if (this.model) { + if (!this.model.badge_type_id) { + this.model.set( + "badge_type_id", + this.get("badgeTypes.firstObject.id") + ); + } + + if (!this.model.badge_grouping_id) { + this.model.set( + "badge_grouping_id", + this.get("badgeGroupings.firstObject.id") + ); + } + + if (!this.model.trigger) { + this.model.set("trigger", this.get("badgeTriggers.firstObject.id")); + } + } + }); + }, + + @discourseComputed("model.query", "buffered.query") + hasQuery(modelQuery, bufferedQuery) { + if (bufferedQuery) { + return bufferedQuery.trim().length > 0; + } + return modelQuery && modelQuery.trim().length > 0; + }, + + @observes("model.id") + _resetSaving: function() { + this.set("saving", false); + this.set("savingStatus", ""); + }, + + actions: { + save() { + if (!this.saving) { + let fields = [ + "allow_title", + "multiple_grant", + "listable", + "auto_revoke", + "enabled", + "show_posts", + "target_posts", + "name", + "description", + "long_description", + "icon", + "image", + "query", + "badge_grouping_id", + "trigger", + "badge_type_id" + ]; + + if (this.get("buffered.system")) { + var protectedFields = this.protectedSystemFields || []; + fields = _.filter(fields, f => !protectedFields.includes(f)); + } + + this.set("saving", true); + this.set("savingStatus", I18n.t("saving")); + + const boolFields = [ + "allow_title", + "multiple_grant", + "listable", + "auto_revoke", + "enabled", + "show_posts", + "target_posts" + ]; + + const data = {}; + const buffered = this.buffered; + fields.forEach(function(field) { + var d = buffered.get(field); + if (boolFields.includes(field)) { + d = !!d; + } + data[field] = d; + }); + + const newBadge = !this.id; + const model = this.model; + this.model + .save(data) + .then(() => { + if (newBadge) { + const adminBadges = this.get("adminBadges.model"); + if (!adminBadges.includes(model)) { + adminBadges.pushObject(model); + } + this.transitionToRoute("adminBadges.show", model.get("id")); + } else { + this.commitBuffer(); + this.set("savingStatus", I18n.t("saved")); + } + }) + .catch(popupAjaxError) + .finally(() => { + this.set("saving", false); + this.set("savingStatus", ""); + }); + } + }, + + destroy() { + const adminBadges = this.get("adminBadges.model"); + const model = this.model; + + if (!model.get("id")) { + this.transitionToRoute("adminBadges.index"); + return; + } + + return bootbox.confirm( + I18n.t("admin.badges.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + model + .destroy() + .then(() => { + adminBadges.removeObject(model); + this.transitionToRoute("adminBadges.index"); + }) + .catch(() => { + bootbox.alert(I18n.t("generic_error")); + }); + } + } + ); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 deleted file mode 100644 index 0586a33505..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 +++ /dev/null @@ -1,166 +0,0 @@ -import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { reads } from "@ember/object/computed"; -import Controller, { inject } from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; -import { propertyNotEqual } from "discourse/lib/computed"; -import { run } from "@ember/runloop"; - -export default Controller.extend(bufferedProperty("model"), { - adminBadges: inject(), - saving: false, - savingStatus: "", - badgeTypes: reads("adminBadges.badgeTypes"), - badgeGroupings: reads("adminBadges.badgeGroupings"), - badgeTriggers: reads("adminBadges.badgeTriggers"), - protectedSystemFields: reads("adminBadges.protectedSystemFields"), - readOnly: reads("buffered.system"), - showDisplayName: propertyNotEqual("name", "displayName"), - - init() { - this._super(...arguments); - - // this is needed because the model doesnt have default values - // and as we are using a bufferedProperty it's not accessible - // in any other way - run.next(() => { - if (this.model) { - if (!this.model.badge_type_id) { - this.model.set( - "badge_type_id", - this.get("badgeTypes.firstObject.id") - ); - } - - if (!this.model.badge_grouping_id) { - this.model.set( - "badge_grouping_id", - this.get("badgeGroupings.firstObject.id") - ); - } - - if (!this.model.trigger) { - this.model.set("trigger", this.get("badgeTriggers.firstObject.id")); - } - } - }); - }, - - @discourseComputed("model.query", "buffered.query") - hasQuery(modelQuery, bufferedQuery) { - if (bufferedQuery) { - return bufferedQuery.trim().length > 0; - } - return modelQuery && modelQuery.trim().length > 0; - }, - - @observes("model.id") - _resetSaving: function() { - this.set("saving", false); - this.set("savingStatus", ""); - }, - - actions: { - save() { - if (!this.saving) { - let fields = [ - "allow_title", - "multiple_grant", - "listable", - "auto_revoke", - "enabled", - "show_posts", - "target_posts", - "name", - "description", - "long_description", - "icon", - "image", - "query", - "badge_grouping_id", - "trigger", - "badge_type_id" - ]; - - if (this.get("buffered.system")) { - var protectedFields = this.protectedSystemFields || []; - fields = _.filter(fields, f => !protectedFields.includes(f)); - } - - this.set("saving", true); - this.set("savingStatus", I18n.t("saving")); - - const boolFields = [ - "allow_title", - "multiple_grant", - "listable", - "auto_revoke", - "enabled", - "show_posts", - "target_posts" - ]; - - const data = {}; - const buffered = this.buffered; - fields.forEach(function(field) { - var d = buffered.get(field); - if (boolFields.includes(field)) { - d = !!d; - } - data[field] = d; - }); - - const newBadge = !this.id; - const model = this.model; - this.model - .save(data) - .then(() => { - if (newBadge) { - const adminBadges = this.get("adminBadges.model"); - if (!adminBadges.includes(model)) { - adminBadges.pushObject(model); - } - this.transitionToRoute("adminBadges.show", model.get("id")); - } else { - this.commitBuffer(); - this.set("savingStatus", I18n.t("saved")); - } - }) - .catch(popupAjaxError) - .finally(() => { - this.set("saving", false); - this.set("savingStatus", ""); - }); - } - }, - - destroy() { - const adminBadges = this.get("adminBadges.model"); - const model = this.model; - - if (!model.get("id")) { - this.transitionToRoute("adminBadges.index"); - return; - } - - return bootbox.confirm( - I18n.t("admin.badges.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - model - .destroy() - .then(() => { - adminBadges.removeObject(model); - this.transitionToRoute("adminBadges.index"); - }) - .catch(() => { - bootbox.alert(I18n.t("generic_error")); - }); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-badges.js.es6 rename to app/assets/javascripts/admin/controllers/admin-badges.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-colors-show.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-colors.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-email-templates.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js b/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js new file mode 100644 index 0000000000..a3e43f2038 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js @@ -0,0 +1,47 @@ +import { not } from "@ember/object/computed"; +import Controller from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; +import { propertyEqual } from "discourse/lib/computed"; + +export default Controller.extend(bufferedProperty("model"), { + saved: false, + isSaving: false, + saveDisabled: propertyEqual("model.robots_txt", "buffered.robots_txt"), + resetDisbaled: not("model.overridden"), + + actions: { + save() { + this.setProperties({ + isSaving: true, + saved: false + }); + + ajax("robots.json", { + type: "PUT", + data: { robots_txt: this.buffered.get("robots_txt") } + }) + .then(data => { + this.commitBuffer(); + this.set("saved", true); + this.set("model.overridden", data.overridden); + }) + .finally(() => this.set("isSaving", false)); + }, + + reset() { + this.setProperties({ + isSaving: true, + saved: false + }); + ajax("robots.json", { type: "DELETE" }) + .then(data => { + this.buffered.set("robots_txt", data.robots_txt); + this.commitBuffer(); + this.set("saved", true); + this.set("model.overridden", false); + }) + .finally(() => this.set("isSaving", false)); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js.es6 deleted file mode 100644 index 5c6d2499f4..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-robots-txt.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -import { not } from "@ember/object/computed"; -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; -import { propertyEqual } from "discourse/lib/computed"; - -export default Controller.extend(bufferedProperty("model"), { - saved: false, - isSaving: false, - saveDisabled: propertyEqual("model.robots_txt", "buffered.robots_txt"), - resetDisbaled: not("model.overridden"), - - actions: { - save() { - this.setProperties({ - isSaving: true, - saved: false - }); - - ajax("robots.json", { - method: "PUT", - data: { robots_txt: this.buffered.get("robots_txt") } - }) - .then(data => { - this.commitBuffer(); - this.set("saved", true); - this.set("model.overridden", data.overridden); - }) - .finally(() => this.set("isSaving", false)); - }, - - reset() { - this.setProperties({ - isSaving: true, - saved: false - }); - ajax("robots.json", { method: "DELETE" }) - .then(data => { - this.buffered.set("robots_txt", data.robots_txt); - this.commitBuffer(); - this.set("saved", true); - this.set("model.overridden", false); - }) - .finally(() => this.set("isSaving", false)); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js new file mode 100644 index 0000000000..006f1728da --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js @@ -0,0 +1,386 @@ +import { makeArray } from "discourse-common/lib/helpers"; +import { + empty, + filterBy, + match, + mapBy, + notEmpty +} from "@ember/object/computed"; +import Controller from "@ember/controller"; +import discourseComputed from "discourse-common/utils/decorators"; +import { url } from "discourse/lib/computed"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import showModal from "discourse/lib/show-modal"; +import ThemeSettings from "admin/models/theme-settings"; +import { THEMES, COMPONENTS } from "admin/models/theme"; +import EmberObject from "@ember/object"; + +const THEME_UPLOAD_VAR = 2; + +export default Controller.extend({ + downloadUrl: url("model.id", "/admin/customize/themes/%@/export"), + previewUrl: url("model.id", "/admin/themes/%@/preview"), + addButtonDisabled: empty("selectedChildThemeId"), + editRouteName: "adminCustomizeThemes.edit", + parentThemesNames: mapBy("model.parentThemes", "name"), + availableParentThemes: filterBy("allThemes", "component", false), + availableActiveParentThemes: filterBy("availableParentThemes", "isActive"), + availableThemesNames: mapBy("availableParentThemes", "name"), + availableActiveThemesNames: mapBy("availableActiveParentThemes", "name"), + availableActiveChildThemes: filterBy("availableChildThemes", "hasParents"), + availableComponentsNames: mapBy("availableChildThemes", "name"), + availableActiveComponentsNames: mapBy("availableActiveChildThemes", "name"), + childThemesNames: mapBy("model.childThemes", "name"), + + @discourseComputed("model.editedFields") + editedFieldsFormatted() { + const descriptions = []; + ["common", "desktop", "mobile"].forEach(target => { + const fields = this.editedFieldsForTarget(target); + if (fields.length < 1) { + return; + } + let resultString = I18n.t("admin.customize.theme." + target); + const formattedFields = fields + .map(f => I18n.t("admin.customize.theme." + f.name + ".text")) + .join(" , "); + resultString += `: ${formattedFields}`; + descriptions.push(resultString); + }); + return descriptions; + }, + + @discourseComputed("colorSchemeId", "model.color_scheme_id") + colorSchemeChanged(colorSchemeId, existingId) { + colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId, 10); + return colorSchemeId !== existingId; + }, + + @discourseComputed("availableChildThemes", "model.childThemes.[]", "model") + selectableChildThemes(available, childThemes) { + if (available) { + const themes = !childThemes + ? available + : available.filter(theme => childThemes.indexOf(theme) === -1); + return themes.length === 0 ? null : themes; + } + }, + + @discourseComputed("model.parentThemes.[]") + relativesSelectorSettingsForComponent() { + return EmberObject.create({ + list_type: "compact", + type: "list", + preview: null, + anyValue: false, + setting: "parent_theme_ids", + label: I18n.t("admin.customize.theme.component_on_themes"), + choices: this.availableThemesNames, + default: this.parentThemesNames.join("|"), + value: this.parentThemesNames.join("|"), + defaultValues: this.availableActiveThemesNames.join("|"), + allThemes: this.allThemes, + setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all_themes") + }); + }, + + @discourseComputed("model.parentThemes.[]") + relativesSelectorSettingsForTheme() { + return EmberObject.create({ + list_type: "compact", + type: "list", + preview: null, + anyValue: false, + setting: "child_theme_ids", + label: I18n.t("admin.customize.theme.included_components"), + choices: this.availableComponentsNames, + default: this.childThemesNames.join("|"), + value: this.childThemesNames.join("|"), + defaultValues: this.availableActiveComponentsNames.join("|"), + allThemes: this.allThemes, + setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all") + }); + }, + + @discourseComputed("allThemes", "model.component", "model") + availableChildThemes(allThemes) { + if (!this.get("model.component")) { + const themeId = this.get("model.id"); + return allThemes.filter( + theme => theme.get("id") !== themeId && theme.get("component") + ); + } + }, + + @discourseComputed("model.component") + convertKey(component) { + const type = component ? "component" : "theme"; + return `admin.customize.theme.convert_${type}`; + }, + + @discourseComputed("model.component") + convertIcon(component) { + return component ? "cube" : ""; + }, + + @discourseComputed("model.component") + convertTooltip(component) { + const type = component ? "component" : "theme"; + return `admin.customize.theme.convert_${type}_tooltip`; + }, + + @discourseComputed("model.settings") + settings(settings) { + return settings.map(setting => ThemeSettings.create(setting)); + }, + + hasSettings: notEmpty("settings"), + + @discourseComputed("model.translations") + translations(translations) { + return translations.map(setting => ThemeSettings.create(setting)); + }, + + hasTranslations: notEmpty("translations"), + + @discourseComputed("model.remoteError", "updatingRemote") + showRemoteError(errorMessage, updating) { + return errorMessage && !updating; + }, + + editedFieldsForTarget(target) { + return this.get("model.editedFields").filter( + field => field.target === target + ); + }, + + commitSwitchType() { + const model = this.model; + const newValue = !model.get("component"); + model.set("component", newValue); + + if (newValue) { + this.set("parentController.currentTab", COMPONENTS); + } else { + this.set("parentController.currentTab", THEMES); + } + + model + .saveChanges("component") + .then(() => { + this.set("colorSchemeId", null); + + model.setProperties({ + default: false, + color_scheme_id: null, + user_selectable: false, + child_themes: [], + childThemes: [] + }); + + this.get("parentController.model.content").forEach(theme => { + const children = makeArray(theme.get("childThemes")); + const rawChildren = makeArray(theme.get("child_themes")); + const index = children ? children.indexOf(model) : -1; + if (index > -1) { + children.splice(index, 1); + rawChildren.splice(index, 1); + theme.setProperties({ + childThemes: children, + child_themes: rawChildren + }); + } + }); + }) + .catch(popupAjaxError); + }, + transitionToEditRoute() { + this.transitionToRoute( + this.editRouteName, + this.get("model.id"), + "common", + "scss" + ); + }, + sourceIsHttp: match("model.remote_theme.remote_url", /^http(s)?:\/\//), + + @discourseComputed( + "model.remote_theme.remote_url", + "model.remote_theme.branch" + ) + remoteThemeLink(remoteThemeUrl, remoteThemeBranch) { + return remoteThemeBranch + ? `${remoteThemeUrl.replace(/\.git$/, "")}/tree/${remoteThemeBranch}` + : remoteThemeUrl; + }, + + actions: { + updateToLatest() { + this.set("updatingRemote", true); + this.model + .updateToLatest() + .catch(popupAjaxError) + .finally(() => { + this.set("updatingRemote", false); + }); + }, + + checkForThemeUpdates() { + this.set("updatingRemote", true); + this.model + .checkForUpdates() + .catch(popupAjaxError) + .finally(() => { + this.set("updatingRemote", false); + }); + }, + + addUploadModal() { + showModal("admin-add-upload", { admin: true, name: "" }); + }, + + addUpload(info) { + let model = this.model; + model.setField("common", info.name, "", info.upload_id, THEME_UPLOAD_VAR); + model.saveChanges("theme_fields").catch(e => popupAjaxError(e)); + }, + + cancelChangeScheme() { + this.set("colorSchemeId", this.get("model.color_scheme_id")); + }, + changeScheme() { + let schemeId = this.colorSchemeId; + this.set( + "model.color_scheme_id", + schemeId === null ? null : parseInt(schemeId, 10) + ); + this.model.saveChanges("color_scheme_id"); + }, + startEditingName() { + this.set("oldName", this.get("model.name")); + this.set("editingName", true); + }, + cancelEditingName() { + this.set("model.name", this.oldName); + this.set("editingName", false); + }, + finishedEditingName() { + this.model.saveChanges("name"); + this.set("editingName", false); + }, + + editTheme() { + if (this.get("model.remote_theme.is_git")) { + bootbox.confirm( + I18n.t("admin.customize.theme.edit_confirm"), + result => { + if (result) { + this.transitionToEditRoute(); + } + } + ); + } else { + this.transitionToEditRoute(); + } + }, + + applyDefault() { + const model = this.model; + model.saveChanges("default").then(() => { + if (model.get("default")) { + this.allThemes.forEach(theme => { + if (theme !== model && theme.get("default")) { + theme.set("default", false); + } + }); + } + }); + }, + + applyUserSelectable() { + this.model.saveChanges("user_selectable"); + }, + + addChildTheme() { + let themeId = parseInt(this.selectedChildThemeId, 10); + let theme = this.allThemes.findBy("id", themeId); + this.model.addChildTheme(theme).then(() => this.store.findAll("theme")); + }, + + removeUpload(upload) { + return bootbox.confirm( + I18n.t("admin.customize.theme.delete_upload_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + this.model.removeField(upload); + } + } + ); + }, + + removeChildTheme(theme) { + this.model + .removeChildTheme(theme) + .then(() => this.store.findAll("theme")); + }, + + destroy() { + return bootbox.confirm( + I18n.t("admin.customize.delete_confirm", { + theme_name: this.get("model.name") + }), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + const model = this.model; + model.setProperties({ recentlyInstalled: false }); + model.destroyRecord().then(() => { + this.allThemes.removeObject(model); + this.transitionToRoute("adminCustomizeThemes"); + }); + } + } + ); + }, + + switchType() { + const relatives = this.get("model.component") + ? this.parentThemes + : this.get("model.childThemes"); + if (relatives && relatives.length > 0) { + const names = relatives.map(relative => relative.get("name")); + bootbox.confirm( + I18n.t(`${this.convertKey}_alert`, { + relatives: names.join(", ") + }), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + this.commitSwitchType(); + } + } + ); + } else { + this.commitSwitchType(); + } + }, + + enableComponent() { + this.model.set("enabled", true); + this.model + .saveChanges("enabled") + .catch(() => this.model.set("enabled", false)); + }, + + disableComponent() { + this.model.set("enabled", false); + this.model + .saveChanges("enabled") + .catch(() => this.model.set("enabled", true)); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 deleted file mode 100644 index a8e25fdec9..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ /dev/null @@ -1,375 +0,0 @@ -import { makeArray } from "discourse-common/lib/helpers"; -import { - empty, - filterBy, - match, - mapBy, - notEmpty -} from "@ember/object/computed"; -import Controller from "@ember/controller"; -import discourseComputed from "discourse-common/utils/decorators"; -import { url } from "discourse/lib/computed"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import showModal from "discourse/lib/show-modal"; -import ThemeSettings from "admin/models/theme-settings"; -import { THEMES, COMPONENTS } from "admin/models/theme"; -import EmberObject from "@ember/object"; - -const THEME_UPLOAD_VAR = 2; - -export default Controller.extend({ - downloadUrl: url("model.id", "/admin/customize/themes/%@/export"), - previewUrl: url("model.id", "/admin/themes/%@/preview"), - addButtonDisabled: empty("selectedChildThemeId"), - editRouteName: "adminCustomizeThemes.edit", - parentThemesNames: mapBy("model.parentThemes", "name"), - availableParentThemes: filterBy("allThemes", "component", false), - availableActiveParentThemes: filterBy("availableParentThemes", "isActive"), - availableThemesNames: mapBy("availableParentThemes", "name"), - availableActiveThemesNames: mapBy("availableActiveParentThemes", "name"), - availableActiveChildThemes: filterBy("availableChildThemes", "hasParents"), - availableComponentsNames: mapBy("availableChildThemes", "name"), - availableActiveComponentsNames: mapBy("availableActiveChildThemes", "name"), - childThemesNames: mapBy("model.childThemes", "name"), - - @discourseComputed("model.editedFields") - editedFieldsFormatted() { - const descriptions = []; - ["common", "desktop", "mobile"].forEach(target => { - const fields = this.editedFieldsForTarget(target); - if (fields.length < 1) { - return; - } - let resultString = I18n.t("admin.customize.theme." + target); - const formattedFields = fields - .map(f => I18n.t("admin.customize.theme." + f.name + ".text")) - .join(" , "); - resultString += `: ${formattedFields}`; - descriptions.push(resultString); - }); - return descriptions; - }, - - @discourseComputed("colorSchemeId", "model.color_scheme_id") - colorSchemeChanged(colorSchemeId, existingId) { - colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId, 10); - return colorSchemeId !== existingId; - }, - - @discourseComputed("availableChildThemes", "model.childThemes.[]", "model") - selectableChildThemes(available, childThemes) { - if (available) { - const themes = !childThemes - ? available - : available.filter(theme => childThemes.indexOf(theme) === -1); - return themes.length === 0 ? null : themes; - } - }, - - @discourseComputed("model.parentThemes.[]") - relativesSelectorSettingsForComponent() { - return EmberObject.create({ - list_type: "compact", - type: "list", - preview: null, - anyValue: false, - setting: "parent_theme_ids", - label: I18n.t("admin.customize.theme.component_on_themes"), - choices: this.availableThemesNames, - default: this.parentThemesNames.join("|"), - value: this.parentThemesNames.join("|"), - defaultValues: this.availableActiveThemesNames.join("|"), - allThemes: this.allThemes, - setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all_themes") - }); - }, - - @discourseComputed("model.parentThemes.[]") - relativesSelectorSettingsForTheme() { - return EmberObject.create({ - list_type: "compact", - type: "list", - preview: null, - anyValue: false, - setting: "child_theme_ids", - label: I18n.t("admin.customize.theme.included_components"), - choices: this.availableComponentsNames, - default: this.childThemesNames.join("|"), - value: this.childThemesNames.join("|"), - defaultValues: this.availableActiveComponentsNames.join("|"), - allThemes: this.allThemes, - setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all") - }); - }, - - @discourseComputed("allThemes", "model.component", "model") - availableChildThemes(allThemes) { - if (!this.get("model.component")) { - const themeId = this.get("model.id"); - return allThemes.filter( - theme => theme.get("id") !== themeId && theme.get("component") - ); - } - }, - - @discourseComputed("model.component") - convertKey(component) { - const type = component ? "component" : "theme"; - return `admin.customize.theme.convert_${type}`; - }, - - @discourseComputed("model.component") - convertIcon(component) { - return component ? "cube" : ""; - }, - - @discourseComputed("model.component") - convertTooltip(component) { - const type = component ? "component" : "theme"; - return `admin.customize.theme.convert_${type}_tooltip`; - }, - - @discourseComputed("model.settings") - settings(settings) { - return settings.map(setting => ThemeSettings.create(setting)); - }, - - hasSettings: notEmpty("settings"), - - @discourseComputed("model.translations") - translations(translations) { - return translations.map(setting => ThemeSettings.create(setting)); - }, - - hasTranslations: notEmpty("translations"), - - @discourseComputed("model.remoteError", "updatingRemote") - showRemoteError(errorMessage, updating) { - return errorMessage && !updating; - }, - - editedFieldsForTarget(target) { - return this.get("model.editedFields").filter( - field => field.target === target - ); - }, - - commitSwitchType() { - const model = this.model; - const newValue = !model.get("component"); - model.set("component", newValue); - - if (newValue) { - this.set("parentController.currentTab", COMPONENTS); - } else { - this.set("parentController.currentTab", THEMES); - } - - model - .saveChanges("component") - .then(() => { - this.set("colorSchemeId", null); - - model.setProperties({ - default: false, - color_scheme_id: null, - user_selectable: false, - child_themes: [], - childThemes: [] - }); - - this.get("parentController.model.content").forEach(theme => { - const children = makeArray(theme.get("childThemes")); - const rawChildren = makeArray(theme.get("child_themes")); - const index = children ? children.indexOf(model) : -1; - if (index > -1) { - children.splice(index, 1); - rawChildren.splice(index, 1); - theme.setProperties({ - childThemes: children, - child_themes: rawChildren - }); - } - }); - }) - .catch(popupAjaxError); - }, - transitionToEditRoute() { - this.transitionToRoute( - this.editRouteName, - this.get("model.id"), - "common", - "scss" - ); - }, - sourceIsHttp: match("model.remote_theme.remote_url", /^http(s)?:\/\//), - actions: { - updateToLatest() { - this.set("updatingRemote", true); - this.model - .updateToLatest() - .catch(popupAjaxError) - .finally(() => { - this.set("updatingRemote", false); - }); - }, - - checkForThemeUpdates() { - this.set("updatingRemote", true); - this.model - .checkForUpdates() - .catch(popupAjaxError) - .finally(() => { - this.set("updatingRemote", false); - }); - }, - - addUploadModal() { - showModal("admin-add-upload", { admin: true, name: "" }); - }, - - addUpload(info) { - let model = this.model; - model.setField("common", info.name, "", info.upload_id, THEME_UPLOAD_VAR); - model.saveChanges("theme_fields").catch(e => popupAjaxError(e)); - }, - - cancelChangeScheme() { - this.set("colorSchemeId", this.get("model.color_scheme_id")); - }, - changeScheme() { - let schemeId = this.colorSchemeId; - this.set( - "model.color_scheme_id", - schemeId === null ? null : parseInt(schemeId, 10) - ); - this.model.saveChanges("color_scheme_id"); - }, - startEditingName() { - this.set("oldName", this.get("model.name")); - this.set("editingName", true); - }, - cancelEditingName() { - this.set("model.name", this.oldName); - this.set("editingName", false); - }, - finishedEditingName() { - this.model.saveChanges("name"); - this.set("editingName", false); - }, - - editTheme() { - if (this.get("model.remote_theme.is_git")) { - bootbox.confirm( - I18n.t("admin.customize.theme.edit_confirm"), - result => { - if (result) { - this.transitionToEditRoute(); - } - } - ); - } else { - this.transitionToEditRoute(); - } - }, - - applyDefault() { - const model = this.model; - model.saveChanges("default").then(() => { - if (model.get("default")) { - this.allThemes.forEach(theme => { - if (theme !== model && theme.get("default")) { - theme.set("default", false); - } - }); - } - }); - }, - - applyUserSelectable() { - this.model.saveChanges("user_selectable"); - }, - - addChildTheme() { - let themeId = parseInt(this.selectedChildThemeId, 10); - let theme = this.allThemes.findBy("id", themeId); - this.model.addChildTheme(theme).then(() => this.store.findAll("theme")); - }, - - removeUpload(upload) { - return bootbox.confirm( - I18n.t("admin.customize.theme.delete_upload_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - this.model.removeField(upload); - } - } - ); - }, - - removeChildTheme(theme) { - this.model - .removeChildTheme(theme) - .then(() => this.store.findAll("theme")); - }, - - destroy() { - return bootbox.confirm( - I18n.t("admin.customize.delete_confirm", { - theme_name: this.get("model.name") - }), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - const model = this.model; - model.setProperties({ recentlyInstalled: false }); - model.destroyRecord().then(() => { - this.allThemes.removeObject(model); - this.transitionToRoute("adminCustomizeThemes"); - }); - } - } - ); - }, - - switchType() { - const relatives = this.get("model.component") - ? this.parentThemes - : this.get("model.childThemes"); - if (relatives && relatives.length > 0) { - const names = relatives.map(relative => relative.get("name")); - bootbox.confirm( - I18n.t(`${this.convertKey}_alert`, { - relatives: names.join(", ") - }), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - this.commitSwitchType(); - } - } - ); - } else { - this.commitSwitchType(); - } - }, - - enableComponent() { - this.model.set("enabled", true); - this.model - .saveChanges("enabled") - .catch(() => this.model.set("enabled", false)); - }, - - disableComponent() { - this.model.set("enabled", false); - this.model - .saveChanges("enabled") - .catch(() => this.model.set("enabled", true)); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 rename to app/assets/javascripts/admin/controllers/admin-customize-themes.js diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-dashboard-general.js.es6 rename to app/assets/javascripts/admin/controllers/admin-dashboard-general.js diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js.es6 rename to app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js new file mode 100644 index 0000000000..d6e15496f7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js @@ -0,0 +1,34 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { debounce } from "@ember/runloop"; +import Controller from "@ember/controller"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +const { get } = Ember; + +export default Controller.extend({ + filter: null, + + @discourseComputed("model.[]", "filter") + filterReports(reports, filter) { + if (filter) { + filter = filter.toLowerCase(); + return reports.filter(report => { + return ( + (get(report, "title") || "").toLowerCase().indexOf(filter) > -1 || + (get(report, "description") || "").toLowerCase().indexOf(filter) > -1 + ); + }); + } + return reports; + }, + + actions: { + filterReports(filter) { + debounce(this, this._performFiltering, filter, INPUT_DELAY); + } + }, + + _performFiltering(filter) { + this.set("filter", filter); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6 deleted file mode 100644 index 9a57b9cf32..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6 +++ /dev/null @@ -1,33 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { debounce } from "@ember/runloop"; -import Controller from "@ember/controller"; - -const { get } = Ember; - -export default Controller.extend({ - filter: null, - - @discourseComputed("model.[]", "filter") - filterReports(reports, filter) { - if (filter) { - filter = filter.toLowerCase(); - return reports.filter(report => { - return ( - (get(report, "title") || "").toLowerCase().indexOf(filter) > -1 || - (get(report, "description") || "").toLowerCase().indexOf(filter) > -1 - ); - }); - } - return reports; - }, - - actions: { - filterReports(filter) { - debounce(this, this._performFiltering, filter, 250); - } - }, - - _performFiltering(filter) { - this.set("filter", filter); - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 rename to app/assets/javascripts/admin/controllers/admin-dashboard.js diff --git a/app/assets/javascripts/admin/controllers/admin-email-advanced-test.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-advanced-test.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-email-advanced-test.js.es6 rename to app/assets/javascripts/admin/controllers/admin-email-advanced-test.js diff --git a/app/assets/javascripts/admin/controllers/admin-email-bounced.js b/app/assets/javascripts/admin/controllers/admin-email-bounced.js new file mode 100644 index 0000000000..bc034db4f7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-bounced.js @@ -0,0 +1,11 @@ +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import discourseDebounce from "discourse/lib/debounce"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default AdminEmailLogsController.extend({ + @observes("filter.{status,user,address,type}") + filterEmailLogs: discourseDebounce(function() { + this.loadLogs(); + }, INPUT_DELAY) +}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 deleted file mode 100644 index 508bc559c5..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import AdminEmailLogsController from "admin/controllers/admin-email-logs"; -import discourseDebounce from "discourse/lib/debounce"; -import { observes } from "discourse-common/utils/decorators"; - -export default AdminEmailLogsController.extend({ - @observes("filter.{status,user,address,type}") - filterEmailLogs: discourseDebounce(function() { - this.loadLogs(); - }, 250) -}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-index.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-email-index.js.es6 rename to app/assets/javascripts/admin/controllers/admin-email-index.js diff --git a/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-logs.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 rename to app/assets/javascripts/admin/controllers/admin-email-logs.js diff --git a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 rename to app/assets/javascripts/admin/controllers/admin-email-preview-digest.js diff --git a/app/assets/javascripts/admin/controllers/admin-email-received.js b/app/assets/javascripts/admin/controllers/admin-email-received.js new file mode 100644 index 0000000000..0942a3dabf --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-received.js @@ -0,0 +1,18 @@ +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import discourseDebounce from "discourse/lib/debounce"; +import IncomingEmail from "admin/models/incoming-email"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default AdminEmailLogsController.extend({ + @observes("filter.{status,from,to,subject}") + filterIncomingEmails: discourseDebounce(function() { + this.loadLogs(IncomingEmail); + }, INPUT_DELAY), + + actions: { + loadMore() { + this.loadLogs(IncomingEmail, true); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 deleted file mode 100644 index 4a3cc363df..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 +++ /dev/null @@ -1,17 +0,0 @@ -import AdminEmailLogsController from "admin/controllers/admin-email-logs"; -import discourseDebounce from "discourse/lib/debounce"; -import IncomingEmail from "admin/models/incoming-email"; -import { observes } from "discourse-common/utils/decorators"; - -export default AdminEmailLogsController.extend({ - @observes("filter.{status,from,to,subject}") - filterIncomingEmails: discourseDebounce(function() { - this.loadLogs(IncomingEmail); - }, 250), - - actions: { - loadMore() { - this.loadLogs(IncomingEmail, true); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-rejected.js b/app/assets/javascripts/admin/controllers/admin-email-rejected.js new file mode 100644 index 0000000000..01882482e7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-rejected.js @@ -0,0 +1,18 @@ +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import discourseDebounce from "discourse/lib/debounce"; +import IncomingEmail from "admin/models/incoming-email"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default AdminEmailLogsController.extend({ + @observes("filter.{status,from,to,subject,error}") + filterIncomingEmails: discourseDebounce(function() { + this.loadLogs(IncomingEmail); + }, INPUT_DELAY), + + actions: { + loadMore() { + this.loadLogs(IncomingEmail, true); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 deleted file mode 100644 index 8c6f6767f4..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 +++ /dev/null @@ -1,17 +0,0 @@ -import AdminEmailLogsController from "admin/controllers/admin-email-logs"; -import discourseDebounce from "discourse/lib/debounce"; -import IncomingEmail from "admin/models/incoming-email"; -import { observes } from "discourse-common/utils/decorators"; - -export default AdminEmailLogsController.extend({ - @observes("filter.{status,from,to,subject,error}") - filterIncomingEmails: discourseDebounce(function() { - this.loadLogs(IncomingEmail); - }, 250), - - actions: { - loadMore() { - this.loadLogs(IncomingEmail, true); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-sent.js b/app/assets/javascripts/admin/controllers/admin-email-sent.js new file mode 100644 index 0000000000..b76fcf81dc --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-sent.js @@ -0,0 +1,11 @@ +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import discourseDebounce from "discourse/lib/debounce"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default AdminEmailLogsController.extend({ + @observes("filter.{status,user,address,type,reply_key}") + filterEmailLogs: discourseDebounce(function() { + this.loadLogs(); + }, INPUT_DELAY) +}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 deleted file mode 100644 index f727c9d6f4..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import AdminEmailLogsController from "admin/controllers/admin-email-logs"; -import discourseDebounce from "discourse/lib/debounce"; -import { observes } from "discourse-common/utils/decorators"; - -export default AdminEmailLogsController.extend({ - @observes("filter.{status,user,address,type,reply_key}") - filterEmailLogs: discourseDebounce(function() { - this.loadLogs(); - }, 250) -}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-skipped.js b/app/assets/javascripts/admin/controllers/admin-email-skipped.js new file mode 100644 index 0000000000..bc034db4f7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-skipped.js @@ -0,0 +1,11 @@ +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import discourseDebounce from "discourse/lib/debounce"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default AdminEmailLogsController.extend({ + @observes("filter.{status,user,address,type}") + filterEmailLogs: discourseDebounce(function() { + this.loadLogs(); + }, INPUT_DELAY) +}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 deleted file mode 100644 index 508bc559c5..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import AdminEmailLogsController from "admin/controllers/admin-email-logs"; -import discourseDebounce from "discourse/lib/debounce"; -import { observes } from "discourse-common/utils/decorators"; - -export default AdminEmailLogsController.extend({ - @observes("filter.{status,user,address,type}") - filterEmailLogs: discourseDebounce(function() { - this.loadLogs(); - }, 250) -}); diff --git a/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 b/app/assets/javascripts/admin/controllers/admin-embedding.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-embedding.js.es6 rename to app/assets/javascripts/admin/controllers/admin-embedding.js diff --git a/app/assets/javascripts/admin/controllers/admin-emojis.js b/app/assets/javascripts/admin/controllers/admin-emojis.js new file mode 100644 index 0000000000..1a49670703 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-emojis.js @@ -0,0 +1,74 @@ +import { sort } from "@ember/object/computed"; +import EmberObject, { action, computed } from "@ember/object"; +import Controller from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; + +const ALL_FILTER = "all"; + +export default Controller.extend({ + filter: null, + sorting: null, + + init() { + this._super(...arguments); + + this.setProperties({ + filter: ALL_FILTER, + sorting: ["group", "name"] + }); + }, + + sortedEmojis: sort("filteredEmojis.[]", "sorting"), + + emojiGroups: computed("model", { + get() { + return this.model.mapBy("group").uniq(); + } + }), + + sortingGroups: computed("emojiGroups.[]", { + get() { + return [ALL_FILTER].concat(this.emojiGroups); + } + }), + + filteredEmojis: computed("model.[]", "filter", { + get() { + if (!this.filter || this.filter === ALL_FILTER) { + return this.model; + } else { + return this.model.filterBy("group", this.filter); + } + } + }), + + @action + filterGroups(value) { + this.set("filter", value); + }, + + @action + emojiUploaded(emoji, group) { + emoji.url += "?t=" + new Date().getTime(); + emoji.group = group; + this.model.pushObject(EmberObject.create(emoji)); + }, + + @action + destroyEmoji(emoji) { + return bootbox.confirm( + I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }), + I18n.t("no_value"), + I18n.t("yes_value"), + destroy => { + if (destroy) { + return ajax("/admin/customize/emojis/" + emoji.get("name"), { + type: "DELETE" + }).then(() => { + this.model.removeObject(emoji); + }); + } + } + ); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 deleted file mode 100644 index 6a1d295759..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -import { sort } from "@ember/object/computed"; -import EmberObject from "@ember/object"; -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -export default Controller.extend({ - sortedEmojis: sort("model", "emojiSorting"), - - init() { - this._super(...arguments); - - this.emojiSorting = ["name"]; - }, - - actions: { - emojiUploaded(emoji) { - emoji.url += "?t=" + new Date().getTime(); - this.model.pushObject(EmberObject.create(emoji)); - }, - - destroy(emoji) { - return bootbox.confirm( - I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }), - I18n.t("no_value"), - I18n.t("yes_value"), - destroy => { - if (destroy) { - return ajax("/admin/customize/emojis/" + emoji.get("name"), { - type: "DELETE" - }).then(() => { - this.model.removeObject(emoji); - }); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js.es6 rename to app/assets/javascripts/admin/controllers/admin-logs-screened-emails.js diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js new file mode 100644 index 0000000000..789f070db1 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js @@ -0,0 +1,136 @@ +import Controller from "@ember/controller"; +import discourseDebounce from "discourse/lib/debounce"; +import { outputExportResult } from "discourse/lib/export-result"; +import { exportEntity } from "discourse/lib/export-csv"; +import ScreenedIpAddress from "admin/models/screened-ip-address"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default Controller.extend({ + loading: false, + filter: null, + savedIpAddress: null, + + @observes("filter") + show: discourseDebounce(function() { + this.set("loading", true); + ScreenedIpAddress.findAll(this.filter).then(result => { + this.setProperties({ model: result, loading: false }); + }); + }, INPUT_DELAY), + + actions: { + allow(record) { + record.set("action_name", "do_nothing"); + record.save(); + }, + + block(record) { + record.set("action_name", "block"); + record.save(); + }, + + edit(record) { + if (!record.get("editing")) { + this.set("savedIpAddress", record.get("ip_address")); + } + record.set("editing", true); + }, + + cancel(record) { + const savedIpAddress = this.savedIpAddress; + if (savedIpAddress && record.get("editing")) { + record.set("ip_address", savedIpAddress); + } + record.set("editing", false); + }, + + save(record) { + const wasEditing = record.get("editing"); + record.set("editing", false); + record + .save() + .then(() => this.set("savedIpAddress", null)) + .catch(e => { + if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { + bootbox.alert( + I18n.t("generic_error_with_reason", { + error: e.jqXHR.responseJSON.errors.join(". ") + }) + ); + } else { + bootbox.alert(I18n.t("generic_error")); + } + if (wasEditing) record.set("editing", true); + }); + }, + + destroy(record) { + return bootbox.confirm( + I18n.t("admin.logs.screened_ips.delete_confirm", { + ip_address: record.get("ip_address") + }), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + record + .destroy() + .then(deleted => { + if (deleted) { + this.model.removeObject(record); + } else { + bootbox.alert(I18n.t("generic_error")); + } + }) + .catch(e => { + bootbox.alert( + I18n.t("generic_error_with_reason", { + error: `http: ${e.status} - ${e.body}` + }) + ); + }); + } + } + ); + }, + + recordAdded(arg) { + this.model.unshiftObject(arg); + }, + + rollUp() { + return bootbox.confirm( + I18n.t("admin.logs.screened_ips.roll_up_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + this.set("loading", true); + return ScreenedIpAddress.rollUp().then(results => { + if (results && results.subnets) { + if (results.subnets.length > 0) { + this.send("show"); + bootbox.alert( + I18n.t("admin.logs.screened_ips.rolled_up_some_subnets", { + subnets: results.subnets.join(", ") + }) + ); + } else { + this.set("loading", false); + bootbox.alert( + I18n.t("admin.logs.screened_ips.rolled_up_no_subnet") + ); + } + } + }); + } + } + ); + }, + + exportScreenedIpList() { + exportEntity("screened_ip").then(outputExportResult); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 deleted file mode 100644 index baee177c02..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 +++ /dev/null @@ -1,135 +0,0 @@ -import Controller from "@ember/controller"; -import discourseDebounce from "discourse/lib/debounce"; -import { outputExportResult } from "discourse/lib/export-result"; -import { exportEntity } from "discourse/lib/export-csv"; -import ScreenedIpAddress from "admin/models/screened-ip-address"; -import { observes } from "discourse-common/utils/decorators"; - -export default Controller.extend({ - loading: false, - filter: null, - savedIpAddress: null, - - @observes("filter") - show: discourseDebounce(function() { - this.set("loading", true); - ScreenedIpAddress.findAll(this.filter).then(result => { - this.setProperties({ model: result, loading: false }); - }); - }, 250), - - actions: { - allow(record) { - record.set("action_name", "do_nothing"); - record.save(); - }, - - block(record) { - record.set("action_name", "block"); - record.save(); - }, - - edit(record) { - if (!record.get("editing")) { - this.set("savedIpAddress", record.get("ip_address")); - } - record.set("editing", true); - }, - - cancel(record) { - const savedIpAddress = this.savedIpAddress; - if (savedIpAddress && record.get("editing")) { - record.set("ip_address", savedIpAddress); - } - record.set("editing", false); - }, - - save(record) { - const wasEditing = record.get("editing"); - record.set("editing", false); - record - .save() - .then(() => this.set("savedIpAddress", null)) - .catch(e => { - if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - bootbox.alert( - I18n.t("generic_error_with_reason", { - error: e.jqXHR.responseJSON.errors.join(". ") - }) - ); - } else { - bootbox.alert(I18n.t("generic_error")); - } - if (wasEditing) record.set("editing", true); - }); - }, - - destroy(record) { - return bootbox.confirm( - I18n.t("admin.logs.screened_ips.delete_confirm", { - ip_address: record.get("ip_address") - }), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - record - .destroy() - .then(deleted => { - if (deleted) { - this.model.removeObject(record); - } else { - bootbox.alert(I18n.t("generic_error")); - } - }) - .catch(e => { - bootbox.alert( - I18n.t("generic_error_with_reason", { - error: `http: ${e.status} - ${e.body}` - }) - ); - }); - } - } - ); - }, - - recordAdded(arg) { - this.model.unshiftObject(arg); - }, - - rollUp() { - return bootbox.confirm( - I18n.t("admin.logs.screened_ips.roll_up_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - this.set("loading", true); - return ScreenedIpAddress.rollUp().then(results => { - if (results && results.subnets) { - if (results.subnets.length > 0) { - this.send("show"); - bootbox.alert( - I18n.t("admin.logs.screened_ips.rolled_up_some_subnets", { - subnets: results.subnets.join(", ") - }) - ); - } else { - this.set("loading", false); - bootbox.alert( - I18n.t("admin.logs.screened_ips.rolled_up_no_subnet") - ); - } - } - }); - } - } - ); - }, - - exportScreenedIpList() { - exportEntity("screened_ip").then(outputExportResult); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js.es6 rename to app/assets/javascripts/admin/controllers/admin-logs-screened-urls.js diff --git a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js new file mode 100644 index 0000000000..7ef84b45ad --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js @@ -0,0 +1,131 @@ +import { gt } from "@ember/object/computed"; +import EmberObject from "@ember/object"; +import { scheduleOnce } from "@ember/runloop"; +import Controller from "@ember/controller"; +import { exportEntity } from "discourse/lib/export-csv"; +import { outputExportResult } from "discourse/lib/export-result"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Controller.extend({ + model: null, + filters: null, + filtersExists: gt("filterCount", 0), + userHistoryActions: null, + + @discourseComputed("filters.action_name") + actionFilter(name) { + return name ? I18n.t("admin.logs.staff_actions.actions." + name) : null; + }, + + resetFilters() { + this.setProperties({ + model: EmberObject.create({ loadingMore: true }), + filters: EmberObject.create() + }); + this.scheduleRefresh(); + }, + + _changeFilters(props) { + this.set("model", EmberObject.create({ loadingMore: true })); + this.filters.setProperties(props); + this.scheduleRefresh(); + }, + + _refresh() { + let filters = this.filters; + let params = {}; + let count = 0; + + // Don't send null values + Object.keys(filters).forEach(k => { + let val = filters.get(k); + if (val) { + params[k] = val; + count += 1; + } + }); + this.set("filterCount", count); + + this.store.findAll("staff-action-log", params).then(result => { + this.set("model", result); + + if (!this.userHistoryActions) { + this.set( + "userHistoryActions", + result.extras.user_history_actions + .map(action => ({ + id: action.id, + action_id: action.action_id, + name: I18n.t("admin.logs.staff_actions.actions." + action.id), + name_raw: action.id + })) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } + }); + }, + + scheduleRefresh() { + scheduleOnce("afterRender", this, this._refresh); + }, + + actions: { + filterActionIdChanged(filterActionId) { + if (filterActionId) { + this._changeFilters({ + action_name: filterActionId, + action_id: this.userHistoryActions.findBy("id", filterActionId) + .action_id + }); + } + }, + + clearFilter(key) { + let changed = {}; + + // Special case, clear all action related stuff + if (key === "actionFilter") { + changed.action_name = null; + changed.action_id = null; + changed.custom_type = null; + this.set("filterActionId", null); + } else { + changed[key] = null; + } + this._changeFilters(changed); + }, + + clearAllFilters() { + this.set("filterActionId", null); + this.resetFilters(); + }, + + filterByAction(logItem) { + this._changeFilters({ + action_name: logItem.get("action_name"), + action_id: logItem.get("action"), + custom_type: logItem.get("custom_type") + }); + }, + + filterByStaffUser(acting_user) { + this._changeFilters({ acting_user: acting_user.username }); + }, + + filterByTargetUser(target_user) { + this._changeFilters({ target_user: target_user.username }); + }, + + filterBySubject(subject) { + this._changeFilters({ subject: subject }); + }, + + exportStaffActionLogs() { + exportEntity("staff_action").then(outputExportResult); + }, + + loadMore() { + this.model.loadMore(); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 deleted file mode 100644 index 849861c921..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 +++ /dev/null @@ -1,132 +0,0 @@ -import { gt } from "@ember/object/computed"; -import EmberObject from "@ember/object"; -import { scheduleOnce } from "@ember/runloop"; -import Controller from "@ember/controller"; -import { exportEntity } from "discourse/lib/export-csv"; -import { outputExportResult } from "discourse/lib/export-result"; -import discourseComputed, { on } from "discourse-common/utils/decorators"; - -export default Controller.extend({ - model: null, - filters: null, - filtersExists: gt("filterCount", 0), - userHistoryActions: null, - - @discourseComputed("filters.action_name") - actionFilter(name) { - return name ? I18n.t("admin.logs.staff_actions.actions." + name) : null; - }, - - @on("init") - resetFilters() { - this.setProperties({ - model: EmberObject.create({ loadingMore: true }), - filters: EmberObject.create() - }); - this.scheduleRefresh(); - }, - - _changeFilters(props) { - this.set("model", EmberObject.create({ loadingMore: true })); - this.filters.setProperties(props); - this.scheduleRefresh(); - }, - - _refresh() { - let filters = this.filters; - let params = {}; - let count = 0; - - // Don't send null values - Object.keys(filters).forEach(k => { - let val = filters.get(k); - if (val) { - params[k] = val; - count += 1; - } - }); - this.set("filterCount", count); - - this.store.findAll("staff-action-log", params).then(result => { - this.set("model", result); - - if (!this.userHistoryActions) { - this.set( - "userHistoryActions", - result.extras.user_history_actions - .map(action => ({ - id: action.id, - action_id: action.action_id, - name: I18n.t("admin.logs.staff_actions.actions." + action.id), - name_raw: action.id - })) - .sort((a, b) => a.name.localeCompare(b.name)) - ); - } - }); - }, - - scheduleRefresh() { - scheduleOnce("afterRender", this, this._refresh); - }, - - actions: { - filterActionIdChanged(filterActionId) { - if (filterActionId) { - this._changeFilters({ - action_name: filterActionId, - action_id: this.userHistoryActions.findBy("id", filterActionId) - .action_id - }); - } - }, - - clearFilter(key) { - let changed = {}; - - // Special case, clear all action related stuff - if (key === "actionFilter") { - changed.action_name = null; - changed.action_id = null; - changed.custom_type = null; - this.set("filterActionId", null); - } else { - changed[key] = null; - } - this._changeFilters(changed); - }, - - clearAllFilters() { - this.set("filterActionId", null); - this.resetFilters(); - }, - - filterByAction(logItem) { - this._changeFilters({ - action_name: logItem.get("action_name"), - action_id: logItem.get("action"), - custom_type: logItem.get("custom_type") - }); - }, - - filterByStaffUser(acting_user) { - this._changeFilters({ acting_user: acting_user.username }); - }, - - filterByTargetUser(target_user) { - this._changeFilters({ target_user: target_user.username }); - }, - - filterBySubject(subject) { - this._changeFilters({ subject: subject }); - }, - - exportStaffActionLogs() { - exportEntity("staff_action").then(outputExportResult); - }, - - loadMore() { - this.model.loadMore(); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-permalinks.js b/app/assets/javascripts/admin/controllers/admin-permalinks.js new file mode 100644 index 0000000000..6f0842049c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-permalinks.js @@ -0,0 +1,48 @@ +import Controller from "@ember/controller"; +import discourseDebounce from "discourse/lib/debounce"; +import Permalink from "admin/models/permalink"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default Controller.extend({ + loading: false, + filter: null, + + @observes("filter") + show: discourseDebounce(function() { + Permalink.findAll(this.filter).then(result => { + this.set("model", result); + this.set("loading", false); + }); + }, INPUT_DELAY), + + actions: { + recordAdded(arg) { + this.model.unshiftObject(arg); + }, + + destroy: function(record) { + return bootbox.confirm( + I18n.t("admin.permalink.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + record.destroy().then( + deleted => { + if (deleted) { + this.model.removeObject(record); + } else { + bootbox.alert(I18n.t("generic_error")); + } + }, + function() { + bootbox.alert(I18n.t("generic_error")); + } + ); + } + } + ); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 b/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 deleted file mode 100644 index a45f8cc89a..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -import Controller from "@ember/controller"; -import discourseDebounce from "discourse/lib/debounce"; -import Permalink from "admin/models/permalink"; -import { observes } from "discourse-common/utils/decorators"; - -export default Controller.extend({ - loading: false, - filter: null, - - @observes("filter") - show: discourseDebounce(function() { - Permalink.findAll(this.filter).then(result => { - this.set("model", result); - this.set("loading", false); - }); - }, 250), - - actions: { - recordAdded(arg) { - this.model.unshiftObject(arg); - }, - - destroy: function(record) { - return bootbox.confirm( - I18n.t("admin.permalink.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - record.destroy().then( - deleted => { - if (deleted) { - this.model.removeObject(record); - } else { - bootbox.alert(I18n.t("generic_error")); - } - }, - function() { - bootbox.alert(I18n.t("generic_error")); - } - ); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-plugins.js.es6 b/app/assets/javascripts/admin/controllers/admin-plugins.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-plugins.js.es6 rename to app/assets/javascripts/admin/controllers/admin-plugins.js diff --git a/app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports-show.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-reports-show.js.es6 rename to app/assets/javascripts/admin/controllers/admin-reports-show.js diff --git a/app/assets/javascripts/admin/controllers/admin-search-logs-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-search-logs-index.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-search-logs-index.js.es6 rename to app/assets/javascripts/admin/controllers/admin-search-logs-index.js diff --git a/app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 b/app/assets/javascripts/admin/controllers/admin-search-logs-term.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 rename to app/assets/javascripts/admin/controllers/admin-search-logs-term.js diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js new file mode 100644 index 0000000000..b2c29730ec --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js @@ -0,0 +1,17 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import Controller, { inject as controller } from "@ember/controller"; + +export default Controller.extend({ + adminSiteSettings: controller(), + categoryNameKey: null, + + @discourseComputed("adminSiteSettings.visibleSiteSettings", "categoryNameKey") + category(categories, nameKey) { + return (categories || []).findBy("nameKey", nameKey); + }, + + @discourseComputed("category") + filteredContent(category) { + return category ? category.siteSettings : []; + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 deleted file mode 100644 index bfd727e6ea..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 +++ /dev/null @@ -1,18 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; - -export default Controller.extend({ - categoryNameKey: null, - adminSiteSettings: inject(), - - @discourseComputed("adminSiteSettings.visibleSiteSettings", "categoryNameKey") - category(categories, nameKey) { - return (categories || []).findBy("nameKey", nameKey); - }, - - @discourseComputed("category") - filteredContent(category) { - return category ? category.siteSettings : []; - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js b/app/assets/javascripts/admin/controllers/admin-site-settings.js new file mode 100644 index 0000000000..ac84553a71 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js @@ -0,0 +1,99 @@ +import { isEmpty } from "@ember/utils"; +import { alias } from "@ember/object/computed"; +import Controller from "@ember/controller"; +import discourseDebounce from "discourse/lib/debounce"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default Controller.extend({ + filter: null, + allSiteSettings: alias("model"), + visibleSiteSettings: null, + onlyOverridden: false, + + filterContentNow(category) { + // If we have no content, don't bother filtering anything + if (!!isEmpty(this.allSiteSettings)) return; + + let filter; + if (this.filter) { + filter = this.filter.toLowerCase().trim(); + } + + if ((!filter || 0 === filter.length) && !this.onlyOverridden) { + this.set("visibleSiteSettings", this.allSiteSettings); + if (this.categoryNameKey === "all_results") { + this.transitionToRoute("adminSiteSettings"); + } + return; + } + + const all = { + nameKey: "all_results", + name: I18n.t("admin.site_settings.categories.all_results"), + siteSettings: [] + }; + const matchesGroupedByCategory = [all]; + + const matches = []; + this.allSiteSettings.forEach(settingsCategory => { + const siteSettings = settingsCategory.siteSettings.filter(item => { + if (this.onlyOverridden && !item.get("overridden")) return false; + if (filter) { + const setting = item.get("setting").toLowerCase(); + return ( + setting.includes(filter) || + setting.replace(/_/g, " ").includes(filter) || + item + .get("description") + .toLowerCase() + .includes(filter) || + (item.get("value") || "").toLowerCase().includes(filter) + ); + } else { + return true; + } + }); + if (siteSettings.length > 0) { + matches.pushObjects(siteSettings); + matchesGroupedByCategory.pushObject({ + nameKey: settingsCategory.nameKey, + name: I18n.t( + "admin.site_settings.categories." + settingsCategory.nameKey + ), + siteSettings, + count: siteSettings.length + }); + } + }); + + all.siteSettings.pushObjects(matches.slice(0, 30)); + all.hasMore = matches.length > 30; + all.count = all.hasMore ? "30+" : matches.length; + + this.set("visibleSiteSettings", matchesGroupedByCategory); + this.transitionToRoute( + "adminSiteSettingsCategory", + category || "all_results" + ); + }, + + @observes("filter", "onlyOverridden", "model") + filterContent: discourseDebounce(function() { + if (this._skipBounce) { + this.set("_skipBounce", false); + } else { + this.filterContentNow(); + } + }, INPUT_DELAY), + + actions: { + clearFilter() { + this.setProperties({ filter: "", onlyOverridden: false }); + }, + + toggleMenu() { + $(".admin-detail").toggleClass("mobile-closed mobile-open"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 deleted file mode 100644 index 28721de995..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 +++ /dev/null @@ -1,98 +0,0 @@ -import { isEmpty } from "@ember/utils"; -import { alias } from "@ember/object/computed"; -import Controller from "@ember/controller"; -import discourseDebounce from "discourse/lib/debounce"; -import { observes } from "discourse-common/utils/decorators"; - -export default Controller.extend({ - filter: null, - allSiteSettings: alias("model"), - visibleSiteSettings: null, - onlyOverridden: false, - - filterContentNow(category) { - // If we have no content, don't bother filtering anything - if (!!isEmpty(this.allSiteSettings)) return; - - let filter; - if (this.filter) { - filter = this.filter.toLowerCase().trim(); - } - - if ((!filter || 0 === filter.length) && !this.onlyOverridden) { - this.set("visibleSiteSettings", this.allSiteSettings); - if (this.categoryNameKey === "all_results") { - this.transitionToRoute("adminSiteSettings"); - } - return; - } - - const all = { - nameKey: "all_results", - name: I18n.t("admin.site_settings.categories.all_results"), - siteSettings: [] - }; - const matchesGroupedByCategory = [all]; - - const matches = []; - this.allSiteSettings.forEach(settingsCategory => { - const siteSettings = settingsCategory.siteSettings.filter(item => { - if (this.onlyOverridden && !item.get("overridden")) return false; - if (filter) { - const setting = item.get("setting").toLowerCase(); - return ( - setting.includes(filter) || - setting.replace(/_/g, " ").includes(filter) || - item - .get("description") - .toLowerCase() - .includes(filter) || - (item.get("value") || "").toLowerCase().includes(filter) - ); - } else { - return true; - } - }); - if (siteSettings.length > 0) { - matches.pushObjects(siteSettings); - matchesGroupedByCategory.pushObject({ - nameKey: settingsCategory.nameKey, - name: I18n.t( - "admin.site_settings.categories." + settingsCategory.nameKey - ), - siteSettings, - count: siteSettings.length - }); - } - }); - - all.siteSettings.pushObjects(matches.slice(0, 30)); - all.hasMore = matches.length > 30; - all.count = all.hasMore ? "30+" : matches.length; - - this.set("visibleSiteSettings", matchesGroupedByCategory); - this.transitionToRoute( - "adminSiteSettingsCategory", - category || "all_results" - ); - }, - - @observes("filter", "onlyOverridden", "model") - filterContent: discourseDebounce(function() { - if (this._skipBounce) { - this.set("_skipBounce", false); - } else { - this.filterContentNow(); - } - }, 250), - - actions: { - clearFilter() { - this.setProperties({ filter: "", onlyOverridden: false }); - }, - - toggleMenu() { - $(".admin-detail").toggleClass("mobile-closed mobile-open"); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 rename to app/assets/javascripts/admin/controllers/admin-site-text-edit.js diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-index.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 rename to app/assets/javascripts/admin/controllers/admin-site-text-index.js diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js b/app/assets/javascripts/admin/controllers/admin-user-badges.js new file mode 100644 index 0000000000..a42bf58ca5 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js @@ -0,0 +1,104 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { alias, sort } from "@ember/object/computed"; +import { next } from "@ember/runloop"; +import Controller, { inject as controller } from "@ember/controller"; +import GrantBadgeController from "discourse/mixins/grant-badge-controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default Controller.extend(GrantBadgeController, { + adminUser: controller(), + user: alias("adminUser.model"), + userBadges: alias("model"), + allBadges: alias("badges"), + sortedBadges: sort("model", "badgeSortOrder"), + + init() { + this._super(...arguments); + + this.badgeSortOrder = ["granted_at:desc"]; + }, + + @discourseComputed("model", "model.[]", "model.expandedBadges.[]") + groupedBadges() { + const allBadges = this.model; + + var grouped = _.groupBy(allBadges, badge => badge.badge_id); + + var expanded = []; + const expandedBadges = allBadges.get("expandedBadges") || []; + + _(grouped).each(function(badges) { + var lastGranted = badges[0].granted_at; + + badges.forEach(badge => { + lastGranted = + lastGranted < badge.granted_at ? badge.granted_at : lastGranted; + }); + + if (badges.length === 1 || expandedBadges.includes(badges[0].badge.id)) { + badges.forEach(badge => expanded.push(badge)); + return; + } + + var result = { + badge: badges[0].badge, + granted_at: lastGranted, + badges: badges, + count: badges.length, + grouped: true + }; + + expanded.push(result); + }); + + return _(expanded) + .sortBy(group => group.granted_at) + .reverse() + .value(); + }, + + actions: { + expandGroup: function(userBadge) { + const model = this.model; + model.set("expandedBadges", model.get("expandedBadges") || []); + model.get("expandedBadges").pushObject(userBadge.badge.id); + }, + + grantBadge() { + this.grantBadge( + this.selectedBadgeId, + this.get("user.username"), + this.badgeReason + ).then( + () => { + this.set("badgeReason", ""); + next(() => { + // Update the selected badge ID after the combobox has re-rendered. + const newSelectedBadge = this.grantableBadges[0]; + if (newSelectedBadge) { + this.set("selectedBadgeId", newSelectedBadge.get("id")); + } + }); + }, + function(error) { + popupAjaxError(error); + } + ); + }, + + revokeBadge(userBadge) { + return bootbox.confirm( + I18n.t("admin.badges.revoke_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + userBadge.revoke().then(() => { + this.model.removeObject(userBadge); + }); + } + } + ); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 deleted file mode 100644 index 3bedafb45c..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ /dev/null @@ -1,105 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { alias, sort } from "@ember/object/computed"; -import { next } from "@ember/runloop"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import GrantBadgeController from "discourse/mixins/grant-badge-controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Controller.extend(GrantBadgeController, { - adminUser: inject(), - user: alias("adminUser.model"), - userBadges: alias("model"), - allBadges: alias("badges"), - sortedBadges: sort("model", "badgeSortOrder"), - - init() { - this._super(...arguments); - - this.badgeSortOrder = ["granted_at:desc"]; - }, - - @discourseComputed("model", "model.[]", "model.expandedBadges.[]") - groupedBadges() { - const allBadges = this.model; - - var grouped = _.groupBy(allBadges, badge => badge.badge_id); - - var expanded = []; - const expandedBadges = allBadges.get("expandedBadges") || []; - - _(grouped).each(function(badges) { - var lastGranted = badges[0].granted_at; - - badges.forEach(badge => { - lastGranted = - lastGranted < badge.granted_at ? badge.granted_at : lastGranted; - }); - - if (badges.length === 1 || expandedBadges.includes(badges[0].badge.id)) { - badges.forEach(badge => expanded.push(badge)); - return; - } - - var result = { - badge: badges[0].badge, - granted_at: lastGranted, - badges: badges, - count: badges.length, - grouped: true - }; - - expanded.push(result); - }); - - return _(expanded) - .sortBy(group => group.granted_at) - .reverse() - .value(); - }, - - actions: { - expandGroup: function(userBadge) { - const model = this.model; - model.set("expandedBadges", model.get("expandedBadges") || []); - model.get("expandedBadges").pushObject(userBadge.badge.id); - }, - - grantBadge() { - this.grantBadge( - this.selectedBadgeId, - this.get("user.username"), - this.badgeReason - ).then( - () => { - this.set("badgeReason", ""); - next(() => { - // Update the selected badge ID after the combobox has re-rendered. - const newSelectedBadge = this.grantableBadges[0]; - if (newSelectedBadge) { - this.set("selectedBadgeId", newSelectedBadge.get("id")); - } - }); - }, - function(error) { - popupAjaxError(error); - } - ); - }, - - revokeBadge(userBadge) { - return bootbox.confirm( - I18n.t("admin.badges.revoke_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - userBadge.revoke().then(() => { - this.model.removeObject(userBadge); - }); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-fields.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 rename to app/assets/javascripts/admin/controllers/admin-user-fields.js diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js b/app/assets/javascripts/admin/controllers/admin-user-index.js new file mode 100644 index 0000000000..9076e56654 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js @@ -0,0 +1,323 @@ +import { notEmpty, and } from "@ember/object/computed"; +import { inject as service } from "@ember/service"; +import Controller from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; +import CanCheckEmails from "discourse/mixins/can-check-emails"; +import { propertyNotEqual, setting } from "discourse/lib/computed"; +import { userPath } from "discourse/lib/url"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import discourseComputed from "discourse-common/utils/decorators"; +import { fmt } from "discourse/lib/computed"; +import { htmlSafe } from "@ember/template"; +import showModal from "discourse/lib/show-modal"; + +export default Controller.extend(CanCheckEmails, { + adminTools: service(), + originalPrimaryGroupId: null, + customGroupIdsBuffer: null, + availableGroups: null, + userTitleValue: null, + + showBadges: setting("enable_badges"), + hasLockedTrustLevel: notEmpty("model.manual_locked_trust_level"), + + primaryGroupDirty: propertyNotEqual( + "originalPrimaryGroupId", + "model.primary_group_id" + ), + + canDisableSecondFactor: and( + "model.second_factor_enabled", + "model.can_disable_second_factor" + ), + + @discourseComputed("model.customGroups") + customGroupIds(customGroups) { + return customGroups.mapBy("id"); + }, + + @discourseComputed("customGroupIdsBuffer", "customGroupIds") + customGroupsDirty(buffer, original) { + if (buffer === null) return false; + + return buffer.length === original.length + ? buffer.any(id => !original.includes(id)) + : true; + }, + + @discourseComputed("model.automaticGroups") + automaticGroups(automaticGroups) { + return automaticGroups + .map(group => { + const name = htmlSafe(group.name); + return `${name}`; + }) + .join(", "); + }, + + @discourseComputed("model.associated_accounts") + associatedAccountsLoaded(associatedAccounts) { + return typeof associatedAccounts !== "undefined"; + }, + + @discourseComputed("model.associated_accounts") + associatedAccounts(associatedAccounts) { + return associatedAccounts + .map(provider => `${provider.name} (${provider.description})`) + .join(", "); + }, + + @discourseComputed("model.user_fields.[]") + userFields(userFields) { + return this.site.collectUserFields(userFields); + }, + + preferencesPath: fmt("model.username_lower", userPath("%@/preferences")), + + @discourseComputed( + "model.can_delete_all_posts", + "model.staff", + "model.post_count" + ) + deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) { + if (canDeleteAllPosts) { + return null; + } + + if (staff) { + return I18n.t("admin.user.delete_posts_forbidden_because_staff"); + } + if (postCount > this.siteSettings.delete_all_posts_max) { + return I18n.t("admin.user.cant_delete_all_too_many_posts", { + count: this.siteSettings.delete_all_posts_max + }); + } else { + return I18n.t("admin.user.cant_delete_all_posts", { + count: this.siteSettings.delete_user_max_post_age + }); + } + }, + + @discourseComputed("model.canBeDeleted", "model.staff") + deleteExplanation(canBeDeleted, staff) { + if (canBeDeleted) { + return null; + } + + if (staff) { + return I18n.t("admin.user.delete_forbidden_because_staff"); + } else { + return I18n.t("admin.user.delete_forbidden", { + count: this.siteSettings.delete_user_max_post_age + }); + } + }, + + groupAdded(added) { + this.model + .groupAdded(added) + .catch(() => bootbox.alert(I18n.t("generic_error"))); + }, + + groupRemoved(groupId) { + this.model + .groupRemoved(groupId) + .then(() => { + if (groupId === this.originalPrimaryGroupId) { + this.set("originalPrimaryGroupId", null); + } + }) + .catch(() => bootbox.alert(I18n.t("generic_error"))); + }, + + actions: { + impersonate() { + return this.model.impersonate(); + }, + logOut() { + return this.model.logOut(); + }, + resetBounceScore() { + return this.model.resetBounceScore(); + }, + approve() { + return this.model.approve(this.currentUser); + }, + deactivate() { + return this.model.deactivate(); + }, + sendActivationEmail() { + return this.model.sendActivationEmail(); + }, + activate() { + return this.model.activate(); + }, + revokeAdmin() { + return this.model.revokeAdmin(); + }, + grantAdmin() { + return this.model.grantAdmin(); + }, + revokeModeration() { + return this.model.revokeModeration(); + }, + grantModeration() { + return this.model.grantModeration(); + }, + saveTrustLevel() { + return this.model.saveTrustLevel(); + }, + restoreTrustLevel() { + return this.model.restoreTrustLevel(); + }, + lockTrustLevel(locked) { + return this.model.lockTrustLevel(locked); + }, + unsilence() { + return this.model.unsilence(); + }, + silence() { + return this.model.silence(); + }, + deleteAllPosts() { + return this.model.deleteAllPosts(); + }, + anonymize() { + return this.model.anonymize(); + }, + disableSecondFactor() { + return this.model.disableSecondFactor(); + }, + + clearPenaltyHistory() { + const user = this.model; + const path = `/admin/users/${user.get("id")}/penalty_history`; + + return ajax(path, { type: "DELETE" }) + .then(() => user.set("tl3_requirements.penalty_counts.total", 0)) + .catch(popupAjaxError); + }, + + destroy() { + const postCount = this.get("model.post_count"); + const maxPostCount = this.siteSettings.delete_all_posts_max; + if (postCount <= maxPostCount) { + return this.model.destroy({ deletePosts: true }); + } else { + return this.model.destroy(); + } + }, + + promptTargetUser() { + showModal("admin-merge-users-prompt", { + admin: true, + model: this.model + }); + }, + + showMergeConfirmation(targetUsername) { + showModal("admin-merge-users-confirmation", { + admin: true, + model: { + username: this.model.username, + targetUsername + } + }); + }, + + merge(targetUsername) { + return this.model.merge({ targetUsername }); + }, + + viewActionLogs() { + this.adminTools.showActionLogs(this, { + target_user: this.get("model.username") + }); + }, + showSuspendModal() { + this.adminTools.showSuspendModal(this.model); + }, + unsuspend() { + this.model.unsuspend().catch(popupAjaxError); + }, + showSilenceModal() { + this.adminTools.showSilenceModal(this.model); + }, + + saveUsername(newUsername) { + const oldUsername = this.get("model.username"); + this.set("model.username", newUsername); + + const path = `/users/${oldUsername.toLowerCase()}/preferences/username`; + + return ajax(path, { data: { new_username: newUsername }, type: "PUT" }) + .catch(e => { + this.set("model.username", oldUsername); + popupAjaxError(e); + }) + .finally(() => this.toggleProperty("editingUsername")); + }, + + saveName(newName) { + const oldName = this.get("model.name"); + this.set("model.name", newName); + + const path = userPath(`${this.get("model.username").toLowerCase()}.json`); + + return ajax(path, { data: { name: newName }, type: "PUT" }) + .catch(e => { + this.set("model.name", oldName); + popupAjaxError(e); + }) + .finally(() => this.toggleProperty("editingName")); + }, + + saveTitle(newTitle) { + const oldTitle = this.get("model.title"); + this.set("model.title", newTitle); + + const path = userPath(`${this.get("model.username").toLowerCase()}.json`); + + return ajax(path, { data: { title: newTitle }, type: "PUT" }) + .catch(e => { + this.set("model.title", oldTitle); + popupAjaxError(e); + }) + .finally(() => this.toggleProperty("editingTitle")); + }, + + saveCustomGroups() { + const currentIds = this.customGroupIds; + const bufferedIds = this.customGroupIdsBuffer; + const availableGroups = this.availableGroups; + + bufferedIds + .filter(id => !currentIds.includes(id)) + .forEach(id => this.groupAdded(availableGroups.findBy("id", id))); + + currentIds + .filter(id => !bufferedIds.includes(id)) + .forEach(id => this.groupRemoved(id)); + }, + + resetCustomGroups() { + this.set("customGroupIdsBuffer", this.model.customGroups.mapBy("id")); + }, + + savePrimaryGroup() { + const primaryGroupId = this.get("model.primary_group_id"); + const path = `/admin/users/${this.get("model.id")}/primary_group`; + + return ajax(path, { + type: "PUT", + data: { primary_group_id: primaryGroupId } + }) + .then(() => this.set("originalPrimaryGroupId", primaryGroupId)) + .catch(() => bootbox.alert(I18n.t("generic_error"))); + }, + + resetPrimaryGroup() { + this.set("model.primary_group_id", this.originalPrimaryGroupId); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 deleted file mode 100644 index 0460c314c7..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ /dev/null @@ -1,300 +0,0 @@ -import { notEmpty, and } from "@ember/object/computed"; -import { inject as service } from "@ember/service"; -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import CanCheckEmails from "discourse/mixins/can-check-emails"; -import { propertyNotEqual, setting } from "discourse/lib/computed"; -import { userPath } from "discourse/lib/url"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import discourseComputed from "discourse-common/utils/decorators"; -import { fmt } from "discourse/lib/computed"; -import { htmlSafe } from "@ember/template"; - -export default Controller.extend(CanCheckEmails, { - adminTools: service(), - originalPrimaryGroupId: null, - customGroupIdsBuffer: null, - availableGroups: null, - userTitleValue: null, - - showBadges: setting("enable_badges"), - hasLockedTrustLevel: notEmpty("model.manual_locked_trust_level"), - - primaryGroupDirty: propertyNotEqual( - "originalPrimaryGroupId", - "model.primary_group_id" - ), - - canDisableSecondFactor: and( - "model.second_factor_enabled", - "model.can_disable_second_factor" - ), - - @discourseComputed("model.customGroups") - customGroupIds(customGroups) { - return customGroups.mapBy("id"); - }, - - @discourseComputed("customGroupIdsBuffer", "customGroupIds") - customGroupsDirty(buffer, original) { - if (buffer === null) return false; - - return buffer.length === original.length - ? buffer.any(id => !original.includes(id)) - : true; - }, - - @discourseComputed("model.automaticGroups") - automaticGroups(automaticGroups) { - return automaticGroups - .map(group => { - const name = htmlSafe(group.name); - return `${name}`; - }) - .join(", "); - }, - - @discourseComputed("model.associated_accounts") - associatedAccountsLoaded(associatedAccounts) { - return typeof associatedAccounts !== "undefined"; - }, - - @discourseComputed("model.associated_accounts") - associatedAccounts(associatedAccounts) { - return associatedAccounts - .map(provider => `${provider.name} (${provider.description})`) - .join(", "); - }, - - @discourseComputed("model.user_fields.[]") - userFields(userFields) { - return this.site.collectUserFields(userFields); - }, - - preferencesPath: fmt("model.username_lower", userPath("%@/preferences")), - - @discourseComputed( - "model.can_delete_all_posts", - "model.staff", - "model.post_count" - ) - deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) { - if (canDeleteAllPosts) { - return null; - } - - if (staff) { - return I18n.t("admin.user.delete_posts_forbidden_because_staff"); - } - if (postCount > this.siteSettings.delete_all_posts_max) { - return I18n.t("admin.user.cant_delete_all_too_many_posts", { - count: this.siteSettings.delete_all_posts_max - }); - } else { - return I18n.t("admin.user.cant_delete_all_posts", { - count: this.siteSettings.delete_user_max_post_age - }); - } - }, - - @discourseComputed("model.canBeDeleted", "model.staff") - deleteExplanation(canBeDeleted, staff) { - if (canBeDeleted) { - return null; - } - - if (staff) { - return I18n.t("admin.user.delete_forbidden_because_staff"); - } else { - return I18n.t("admin.user.delete_forbidden", { - count: this.siteSettings.delete_user_max_post_age - }); - } - }, - - groupAdded(added) { - this.model - .groupAdded(added) - .catch(() => bootbox.alert(I18n.t("generic_error"))); - }, - - groupRemoved(groupId) { - this.model - .groupRemoved(groupId) - .then(() => { - if (groupId === this.originalPrimaryGroupId) { - this.set("originalPrimaryGroupId", null); - } - }) - .catch(() => bootbox.alert(I18n.t("generic_error"))); - }, - - actions: { - impersonate() { - return this.model.impersonate(); - }, - logOut() { - return this.model.logOut(); - }, - resetBounceScore() { - return this.model.resetBounceScore(); - }, - approve() { - return this.model.approve(this.currentUser); - }, - deactivate() { - return this.model.deactivate(); - }, - sendActivationEmail() { - return this.model.sendActivationEmail(); - }, - activate() { - return this.model.activate(); - }, - revokeAdmin() { - return this.model.revokeAdmin(); - }, - grantAdmin() { - return this.model.grantAdmin(); - }, - revokeModeration() { - return this.model.revokeModeration(); - }, - grantModeration() { - return this.model.grantModeration(); - }, - saveTrustLevel() { - return this.model.saveTrustLevel(); - }, - restoreTrustLevel() { - return this.model.restoreTrustLevel(); - }, - lockTrustLevel(locked) { - return this.model.lockTrustLevel(locked); - }, - unsilence() { - return this.model.unsilence(); - }, - silence() { - return this.model.silence(); - }, - deleteAllPosts() { - return this.model.deleteAllPosts(); - }, - anonymize() { - return this.model.anonymize(); - }, - disableSecondFactor() { - return this.model.disableSecondFactor(); - }, - - clearPenaltyHistory() { - const user = this.model; - const path = `/admin/users/${user.get("id")}/penalty_history`; - - return ajax(path, { type: "DELETE" }) - .then(() => user.set("tl3_requirements.penalty_counts.total", 0)) - .catch(popupAjaxError); - }, - - destroy() { - const postCount = this.get("model.post_count"); - if (postCount <= 5) { - return this.model.destroy({ deletePosts: true }); - } else { - return this.model.destroy(); - } - }, - - viewActionLogs() { - this.adminTools.showActionLogs(this, { - target_user: this.get("model.username") - }); - }, - showSuspendModal() { - this.adminTools.showSuspendModal(this.model); - }, - unsuspend() { - this.model.unsuspend().catch(popupAjaxError); - }, - showSilenceModal() { - this.adminTools.showSilenceModal(this.model); - }, - - saveUsername(newUsername) { - const oldUsername = this.get("model.username"); - this.set("model.username", newUsername); - - const path = `/users/${oldUsername.toLowerCase()}/preferences/username`; - - return ajax(path, { data: { new_username: newUsername }, type: "PUT" }) - .catch(e => { - this.set("model.username", oldUsername); - popupAjaxError(e); - }) - .finally(() => this.toggleProperty("editingUsername")); - }, - - saveName(newName) { - const oldName = this.get("model.name"); - this.set("model.name", newName); - - const path = userPath(`${this.get("model.username").toLowerCase()}.json`); - - return ajax(path, { data: { name: newName }, type: "PUT" }) - .catch(e => { - this.set("model.name", oldName); - popupAjaxError(e); - }) - .finally(() => this.toggleProperty("editingName")); - }, - - saveTitle(newTitle) { - const oldTitle = this.get("model.title"); - this.set("model.title", newTitle); - - const path = userPath(`${this.get("model.username").toLowerCase()}.json`); - - return ajax(path, { data: { title: newTitle }, type: "PUT" }) - .catch(e => { - this.set("model.title", oldTitle); - popupAjaxError(e); - }) - .finally(() => this.toggleProperty("editingTitle")); - }, - - saveCustomGroups() { - const currentIds = this.customGroupIds; - const bufferedIds = this.customGroupIdsBuffer; - const availableGroups = this.availableGroups; - - bufferedIds - .filter(id => !currentIds.includes(id)) - .forEach(id => this.groupAdded(availableGroups.findBy("id", id))); - - currentIds - .filter(id => !bufferedIds.includes(id)) - .forEach(id => this.groupRemoved(id)); - }, - - resetCustomGroups() { - this.set("customGroupIdsBuffer", this.model.customGroups.mapBy("id")); - }, - - savePrimaryGroup() { - const primaryGroupId = this.get("model.primary_group_id"); - const path = `/admin/users/${this.get("model.id")}/primary_group`; - - return ajax(path, { - type: "PUT", - data: { primary_group_id: primaryGroupId } - }) - .then(() => this.set("originalPrimaryGroupId", primaryGroupId)) - .catch(() => bootbox.alert(I18n.t("generic_error"))); - }, - - resetPrimaryGroup() { - this.set("model.primary_group_id", this.originalPrimaryGroupId); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-user.js.es6 b/app/assets/javascripts/admin/controllers/admin-user.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-user.js.es6 rename to app/assets/javascripts/admin/controllers/admin-user.js diff --git a/app/assets/javascripts/admin/controllers/admin-users-list-show.js b/app/assets/javascripts/admin/controllers/admin-users-list-show.js new file mode 100644 index 0000000000..8b7adb194c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-users-list-show.js @@ -0,0 +1,81 @@ +import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import Controller from "@ember/controller"; +import discourseDebounce from "discourse/lib/debounce"; +import { i18n } from "discourse/lib/computed"; +import AdminUser from "admin/models/admin-user"; +import CanCheckEmails from "discourse/mixins/can-check-emails"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default Controller.extend(CanCheckEmails, { + model: null, + query: null, + order: null, + ascending: null, + showEmails: false, + refreshing: false, + listFilter: null, + selectAll: false, + searchHint: i18n("search_hint"), + + init() { + this._super(...arguments); + + this._page = 1; + this._results = []; + this._canLoadMore = true; + }, + + @discourseComputed("query") + title(query) { + return I18n.t("admin.users.titles." + query); + }, + + @observes("listFilter") + _filterUsers: discourseDebounce(function() { + this.resetFilters(); + }, INPUT_DELAY), + + resetFilters() { + this._page = 1; + this._results = []; + this._canLoadMore = true; + this._refreshUsers(); + }, + + _refreshUsers() { + if (!this._canLoadMore) { + return; + } + + this.set("refreshing", true); + + AdminUser.findAll(this.query, { + filter: this.listFilter, + show_emails: this.showEmails, + order: this.order, + ascending: this.ascending, + page: this._page + }) + .then(result => { + if (!result || result.length === 0) { + this._canLoadMore = false; + } + + this._results = this._results.concat(result); + this.set("model", this._results); + }) + .finally(() => this.set("refreshing", false)); + }, + + actions: { + loadMore() { + this._page += 1; + this._refreshUsers(); + }, + + toggleEmailVisibility() { + this.toggleProperty("showEmails"); + this.resetFilters(); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 deleted file mode 100644 index 8776fee466..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 +++ /dev/null @@ -1,80 +0,0 @@ -import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import Controller from "@ember/controller"; -import discourseDebounce from "discourse/lib/debounce"; -import { i18n } from "discourse/lib/computed"; -import AdminUser from "admin/models/admin-user"; -import CanCheckEmails from "discourse/mixins/can-check-emails"; - -export default Controller.extend(CanCheckEmails, { - model: null, - query: null, - order: null, - ascending: null, - showEmails: false, - refreshing: false, - listFilter: null, - selectAll: false, - searchHint: i18n("search_hint"), - - init() { - this._super(...arguments); - - this._page = 1; - this._results = []; - this._canLoadMore = true; - }, - - @discourseComputed("query") - title(query) { - return I18n.t("admin.users.titles." + query); - }, - - @observes("listFilter") - _filterUsers: discourseDebounce(function() { - this.resetFilters(); - }, 250), - - resetFilters() { - this._page = 1; - this._results = []; - this._canLoadMore = true; - this._refreshUsers(); - }, - - _refreshUsers() { - if (!this._canLoadMore) { - return; - } - - this.set("refreshing", true); - - AdminUser.findAll(this.query, { - filter: this.listFilter, - show_emails: this.showEmails, - order: this.order, - ascending: this.ascending, - page: this._page - }) - .then(result => { - if (!result || result.length === 0) { - this._canLoadMore = false; - } - - this._results = this._results.concat(result); - this.set("model", this._results); - }) - .finally(() => this.set("refreshing", false)); - }, - - actions: { - loadMore() { - this._page += 1; - this._refreshUsers(); - }, - - toggleEmailVisibility() { - this.toggleProperty("showEmails"); - this.resetFilters(); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js new file mode 100644 index 0000000000..cf5815da73 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js @@ -0,0 +1,120 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { or } from "@ember/object/computed"; +import { schedule } from "@ember/runloop"; +import Controller, { inject as controller } from "@ember/controller"; +import WatchedWord from "admin/models/watched-word"; +import { ajax } from "discourse/lib/ajax"; +import { fmt } from "discourse/lib/computed"; +import showModal from "discourse/lib/show-modal"; + +export default Controller.extend({ + adminWatchedWords: controller(), + actionNameKey: null, + showWordsList: or( + "adminWatchedWords.filtered", + "adminWatchedWords.showWords" + ), + downloadLink: fmt( + "actionNameKey", + "/admin/logs/watched_words/action/%@/download" + ), + + findAction(actionName) { + return (this.get("adminWatchedWords.model") || []).findBy( + "nameKey", + actionName + ); + }, + + @discourseComputed("actionNameKey", "adminWatchedWords.model") + currentAction(actionName) { + return this.findAction(actionName); + }, + + @discourseComputed("currentAction.words.[]", "adminWatchedWords.model") + filteredContent(words) { + return words || []; + }, + + @discourseComputed("actionNameKey") + actionDescription(actionNameKey) { + return I18n.t("admin.watched_words.action_descriptions." + actionNameKey); + }, + + @discourseComputed("currentAction.count") + wordCount(count) { + return count || 0; + }, + + actions: { + recordAdded(arg) { + const a = this.findAction(this.actionNameKey); + if (a) { + a.words.unshiftObject(arg); + a.incrementProperty("count"); + schedule("afterRender", () => { + // remove from other actions lists + let match = null; + this.get("adminWatchedWords.model").forEach(action => { + if (match) return; + + if (action.nameKey !== this.actionNameKey) { + match = action.words.findBy("id", arg.id); + if (match) { + action.words.removeObject(match); + action.decrementProperty("count"); + } + } + }); + }); + } + }, + + recordRemoved(arg) { + if (this.currentAction) { + this.currentAction.words.removeObject(arg); + this.currentAction.decrementProperty("count"); + } + }, + + uploadComplete() { + WatchedWord.findAll().then(data => { + this.set("adminWatchedWords.model", data); + }); + }, + + clearAll() { + const actionKey = this.actionNameKey; + bootbox.confirm( + I18n.t(`admin.watched_words.clear_all_confirm_${actionKey}`), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + ajax(`/admin/logs/watched_words/action/${actionKey}.json`, { + type: "DELETE" + }).then(() => { + const action = this.findAction(actionKey); + if (action) { + action.setProperties({ + words: [], + count: 0 + }); + } + }); + } + } + ); + }, + + test() { + WatchedWord.findAll().then(data => { + this.set("adminWatchedWords.model", data); + showModal("admin-watched-word-test", { + admin: true, + model: this.currentAction + }); + }); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 deleted file mode 100644 index d6f52c13fd..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 +++ /dev/null @@ -1,121 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { or } from "@ember/object/computed"; -import { schedule } from "@ember/runloop"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import WatchedWord from "admin/models/watched-word"; -import { ajax } from "discourse/lib/ajax"; -import { fmt } from "discourse/lib/computed"; -import showModal from "discourse/lib/show-modal"; - -export default Controller.extend({ - actionNameKey: null, - adminWatchedWords: inject(), - showWordsList: or( - "adminWatchedWords.filtered", - "adminWatchedWords.showWords" - ), - downloadLink: fmt( - "actionNameKey", - "/admin/logs/watched_words/action/%@/download" - ), - - findAction(actionName) { - return (this.get("adminWatchedWords.model") || []).findBy( - "nameKey", - actionName - ); - }, - - @discourseComputed("actionNameKey", "adminWatchedWords.model") - currentAction(actionName) { - return this.findAction(actionName); - }, - - @discourseComputed("currentAction.words.[]", "adminWatchedWords.model") - filteredContent(words) { - return words || []; - }, - - @discourseComputed("actionNameKey") - actionDescription(actionNameKey) { - return I18n.t("admin.watched_words.action_descriptions." + actionNameKey); - }, - - @discourseComputed("currentAction.count") - wordCount(count) { - return count || 0; - }, - - actions: { - recordAdded(arg) { - const a = this.findAction(this.actionNameKey); - if (a) { - a.words.unshiftObject(arg); - a.incrementProperty("count"); - schedule("afterRender", () => { - // remove from other actions lists - let match = null; - this.get("adminWatchedWords.model").forEach(action => { - if (match) return; - - if (action.nameKey !== this.actionNameKey) { - match = action.words.findBy("id", arg.id); - if (match) { - action.words.removeObject(match); - action.decrementProperty("count"); - } - } - }); - }); - } - }, - - recordRemoved(arg) { - if (this.currentAction) { - this.currentAction.words.removeObject(arg); - this.currentAction.decrementProperty("count"); - } - }, - - uploadComplete() { - WatchedWord.findAll().then(data => { - this.set("adminWatchedWords.model", data); - }); - }, - - clearAll() { - const actionKey = this.actionNameKey; - bootbox.confirm( - I18n.t(`admin.watched_words.clear_all_confirm_${actionKey}`), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - ajax(`/admin/logs/watched_words/action/${actionKey}.json`, { - method: "DELETE" - }).then(() => { - const action = this.findAction(actionKey); - if (action) { - action.setProperties({ - words: [], - count: 0 - }); - } - }); - } - } - ); - }, - - test() { - WatchedWord.findAll().then(data => { - this.set("adminWatchedWords.model", data); - showModal("admin-watched-word-test", { - admin: true, - model: this.currentAction - }); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words.js b/app/assets/javascripts/admin/controllers/admin-watched-words.js new file mode 100644 index 0000000000..636358c629 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-watched-words.js @@ -0,0 +1,63 @@ +import { isEmpty } from "@ember/utils"; +import { alias } from "@ember/object/computed"; +import EmberObject from "@ember/object"; +import Controller from "@ember/controller"; +import discourseDebounce from "discourse/lib/debounce"; +import { observes } from "discourse-common/utils/decorators"; +import { INPUT_DELAY } from "discourse-common/config/environment"; + +export default Controller.extend({ + filter: null, + filtered: false, + showWords: false, + disableShowWords: alias("filtered"), + regularExpressions: null, + + filterContentNow() { + if (!!isEmpty(this.allWatchedWords)) return; + + let filter; + if (this.filter) { + filter = this.filter.toLowerCase(); + } + + if (filter === undefined || filter.length < 1) { + this.set("model", this.allWatchedWords); + return; + } + + const matchesByAction = []; + + this.allWatchedWords.forEach(wordsForAction => { + const wordRecords = wordsForAction.words.filter(wordRecord => { + return wordRecord.word.indexOf(filter) > -1; + }); + matchesByAction.pushObject( + EmberObject.create({ + nameKey: wordsForAction.nameKey, + name: wordsForAction.name, + words: wordRecords, + count: wordRecords.length + }) + ); + }); + + this.set("model", matchesByAction); + }, + + @observes("filter") + filterContent: discourseDebounce(function() { + this.filterContentNow(); + this.set("filtered", !isEmpty(this.filter)); + }, INPUT_DELAY), + + actions: { + clearFilter() { + this.setProperties({ filter: "" }); + }, + + toggleMenu() { + $(".admin-detail").toggleClass("mobile-closed mobile-open"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 deleted file mode 100644 index dbc2afe1dc..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 +++ /dev/null @@ -1,62 +0,0 @@ -import { isEmpty } from "@ember/utils"; -import { alias } from "@ember/object/computed"; -import EmberObject from "@ember/object"; -import Controller from "@ember/controller"; -import discourseDebounce from "discourse/lib/debounce"; -import { observes } from "discourse-common/utils/decorators"; - -export default Controller.extend({ - filter: null, - filtered: false, - showWords: false, - disableShowWords: alias("filtered"), - regularExpressions: null, - - filterContentNow() { - if (!!isEmpty(this.allWatchedWords)) return; - - let filter; - if (this.filter) { - filter = this.filter.toLowerCase(); - } - - if (filter === undefined || filter.length < 1) { - this.set("model", this.allWatchedWords); - return; - } - - const matchesByAction = []; - - this.allWatchedWords.forEach(wordsForAction => { - const wordRecords = wordsForAction.words.filter(wordRecord => { - return wordRecord.word.indexOf(filter) > -1; - }); - matchesByAction.pushObject( - EmberObject.create({ - nameKey: wordsForAction.nameKey, - name: wordsForAction.name, - words: wordRecords, - count: wordRecords.length - }) - ); - }); - - this.set("model", matchesByAction); - }, - - @observes("filter") - filterContent: discourseDebounce(function() { - this.filterContentNow(); - this.set("filtered", !isEmpty(this.filter)); - }, 250), - - actions: { - clearFilter() { - this.setProperties({ filter: "" }); - }, - - toggleMenu() { - $(".admin-detail").toggleClass("mobile-closed mobile-open"); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-web-hooks-show-events.js.es6 b/app/assets/javascripts/admin/controllers/admin-web-hooks-show-events.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-web-hooks-show-events.js.es6 rename to app/assets/javascripts/admin/controllers/admin-web-hooks-show-events.js diff --git a/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js b/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js new file mode 100644 index 0000000000..4332f52d9c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js @@ -0,0 +1,148 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { isEmpty } from "@ember/utils"; +import { alias } from "@ember/object/computed"; +import Controller, { inject as controller } from "@ember/controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { extractDomainFromUrl } from "discourse/lib/utilities"; +import EmberObject from "@ember/object"; + +export default Controller.extend({ + adminWebHooks: controller(), + eventTypes: alias("adminWebHooks.eventTypes"), + defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), + contentTypes: alias("adminWebHooks.contentTypes"), + + @discourseComputed + showTagsFilter() { + return this.siteSettings.tagging_enabled; + }, + + @discourseComputed("model.isSaving", "saved", "saveButtonDisabled") + savingStatus(isSaving, saved, saveButtonDisabled) { + if (isSaving) { + return I18n.t("saving"); + } else if (!saveButtonDisabled && saved) { + return I18n.t("saved"); + } + // Use side effect of validation to clear saved text + this.set("saved", false); + return ""; + }, + + @discourseComputed("model.isNew") + saveButtonText(isNew) { + return isNew + ? I18n.t("admin.web_hooks.create") + : I18n.t("admin.web_hooks.save"); + }, + + @discourseComputed("model.secret") + secretValidation(secret) { + if (!isEmpty(secret)) { + if (secret.indexOf(" ") !== -1) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.secret_invalid") + }); + } + + if (secret.length < 12) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.secret_too_short") + }); + } + } + }, + + @discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]") + eventTypeValidation(isWildcard, eventTypes) { + if (!isWildcard && isEmpty(eventTypes)) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.event_type_missing") + }); + } + }, + + @discourseComputed( + "model.isSaving", + "secretValidation", + "eventTypeValidation", + "model.payload_url" + ) + saveButtonDisabled( + isSaving, + secretValidation, + eventTypeValidation, + payloadUrl + ) { + return isSaving + ? false + : secretValidation || eventTypeValidation || isEmpty(payloadUrl); + }, + + actions: { + save() { + this.set("saved", false); + const url = this.get("model.payload_url"); + const domain = extractDomainFromUrl(url); + const model = this.model; + const isNew = model.get("isNew"); + + const saveWebHook = () => { + return model + .save() + .then(() => { + this.set("saved", true); + this.adminWebHooks.get("model").addObject(model); + + if (isNew) { + this.transitionToRoute("adminWebHooks.show", model.get("id")); + } + }) + .catch(popupAjaxError); + }; + + if ( + domain === "localhost" || + domain.match(/192\.168\.\d+\.\d+/) || + domain.match(/127\.\d+\.\d+\.\d+/) || + url.startsWith(Discourse.BaseUrl) + ) { + return bootbox.confirm( + I18n.t("admin.web_hooks.warn_local_payload_url"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + return saveWebHook(); + } + } + ); + } + + return saveWebHook(); + }, + + destroy() { + return bootbox.confirm( + I18n.t("admin.web_hooks.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + const model = this.model; + model + .destroyRecord() + .then(() => { + this.adminWebHooks.get("model").removeObject(model); + this.transitionToRoute("adminWebHooks"); + }) + .catch(popupAjaxError); + } + } + ); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js.es6 deleted file mode 100644 index 4ba34034f3..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js.es6 +++ /dev/null @@ -1,149 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { isEmpty } from "@ember/utils"; -import { alias } from "@ember/object/computed"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { extractDomainFromUrl } from "discourse/lib/utilities"; -import EmberObject from "@ember/object"; - -export default Controller.extend({ - adminWebHooks: inject(), - eventTypes: alias("adminWebHooks.eventTypes"), - defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), - contentTypes: alias("adminWebHooks.contentTypes"), - - @discourseComputed - showTagsFilter() { - return this.siteSettings.tagging_enabled; - }, - - @discourseComputed("model.isSaving", "saved", "saveButtonDisabled") - savingStatus(isSaving, saved, saveButtonDisabled) { - if (isSaving) { - return I18n.t("saving"); - } else if (!saveButtonDisabled && saved) { - return I18n.t("saved"); - } - // Use side effect of validation to clear saved text - this.set("saved", false); - return ""; - }, - - @discourseComputed("model.isNew") - saveButtonText(isNew) { - return isNew - ? I18n.t("admin.web_hooks.create") - : I18n.t("admin.web_hooks.save"); - }, - - @discourseComputed("model.secret") - secretValidation(secret) { - if (!isEmpty(secret)) { - if (secret.indexOf(" ") !== -1) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.secret_invalid") - }); - } - - if (secret.length < 12) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.secret_too_short") - }); - } - } - }, - - @discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]") - eventTypeValidation(isWildcard, eventTypes) { - if (!isWildcard && isEmpty(eventTypes)) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.event_type_missing") - }); - } - }, - - @discourseComputed( - "model.isSaving", - "secretValidation", - "eventTypeValidation", - "model.payload_url" - ) - saveButtonDisabled( - isSaving, - secretValidation, - eventTypeValidation, - payloadUrl - ) { - return isSaving - ? false - : secretValidation || eventTypeValidation || isEmpty(payloadUrl); - }, - - actions: { - save() { - this.set("saved", false); - const url = this.get("model.payload_url"); - const domain = extractDomainFromUrl(url); - const model = this.model; - const isNew = model.get("isNew"); - - const saveWebHook = () => { - return model - .save() - .then(() => { - this.set("saved", true); - this.adminWebHooks.get("model").addObject(model); - - if (isNew) { - this.transitionToRoute("adminWebHooks.show", model.get("id")); - } - }) - .catch(popupAjaxError); - }; - - if ( - domain === "localhost" || - domain.match(/192\.168\.\d+\.\d+/) || - domain.match(/127\.\d+\.\d+\.\d+/) || - url.startsWith(Discourse.BaseUrl) - ) { - return bootbox.confirm( - I18n.t("admin.web_hooks.warn_local_payload_url"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - return saveWebHook(); - } - } - ); - } - - return saveWebHook(); - }, - - destroy() { - return bootbox.confirm( - I18n.t("admin.web_hooks.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - const model = this.model; - model - .destroyRecord() - .then(() => { - this.adminWebHooks.get("model").removeObject(model); - this.transitionToRoute("adminWebHooks"); - }) - .catch(popupAjaxError); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-web-hooks.js.es6 b/app/assets/javascripts/admin/controllers/admin-web-hooks.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin-web-hooks.js.es6 rename to app/assets/javascripts/admin/controllers/admin-web-hooks.js diff --git a/app/assets/javascripts/admin/controllers/admin.js.es6 b/app/assets/javascripts/admin/controllers/admin.js similarity index 100% rename from app/assets/javascripts/admin/controllers/admin.js.es6 rename to app/assets/javascripts/admin/controllers/admin.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js new file mode 100644 index 0000000000..6fb2859618 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js @@ -0,0 +1,141 @@ +import { isEmpty } from "@ember/utils"; +import { and, not } from "@ember/object/computed"; +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { ajax } from "discourse/lib/ajax"; +import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4]; + +const SCSS_VARIABLE_NAMES = [ + // common/foundation/colors.scss + "primary", + "secondary", + "tertiary", + "quaternary", + "header_background", + "header_primary", + "highlight", + "danger", + "success", + "love", + // common/foundation/math.scss + "E", + "PI", + "LN2", + "SQRT2", + // common/foundation/variables.scss + "small-width", + "medium-width", + "large-width", + "google", + "instagram", + "facebook", + "cas", + "twitter", + "github", + "base-font-size", + "base-line-height", + "base-font-family", + "primary-low", + "primary-medium", + "secondary-low", + "secondary-medium", + "tertiary-low", + "quaternary-low", + "highlight-low", + "highlight-medium", + "danger-low", + "danger-medium", + "success-low", + "love-low" +]; + +export default Controller.extend(ModalFunctionality, { + adminCustomizeThemesShow: controller(), + + uploadUrl: "/admin/themes/upload_asset", + + onShow() { + this.set("name", null); + this.set("fileSelected", false); + }, + + enabled: and("nameValid", "fileSelected"), + disabled: not("enabled"), + + @discourseComputed("name", "adminCustomizeThemesShow.model.theme_fields") + errorMessage(name, themeFields) { + if (name) { + if (!name.match(/^[a-z_][a-z0-9_-]*$/i)) { + return I18n.t( + "admin.customize.theme.variable_name_error.invalid_syntax" + ); + } else if (SCSS_VARIABLE_NAMES.includes(name.toLowerCase())) { + return I18n.t("admin.customize.theme.variable_name_error.no_overwrite"); + } else if ( + themeFields.some( + tf => + THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && + name === tf.name + ) + ) { + return I18n.t( + "admin.customize.theme.variable_name_error.must_be_unique" + ); + } + } + + return null; + }, + + @discourseComputed("errorMessage") + nameValid(errorMessage) { + return null === errorMessage; + }, + + @observes("name") + uploadChanged() { + const file = $("#file-input")[0]; + this.set("fileSelected", file && file.files[0]); + }, + + actions: { + updateName() { + let name = this.name; + if (isEmpty(name)) { + name = $("#file-input")[0].files[0].name; + this.set("name", name.split(".")[0]); + } + this.uploadChanged(); + }, + + upload() { + const file = $("#file-input")[0].files[0]; + + const options = { + type: "POST", + processData: false, + contentType: false, + data: new FormData() + }; + + options.data.append("file", file); + + ajax(this.uploadUrl, options) + .then(result => { + const upload = { + upload_id: result.upload_id, + name: this.name, + original_filename: file.name + }; + this.adminCustomizeThemesShow.send("addUpload", upload); + this.send("closeModal"); + }) + .catch(e => { + popupAjaxError(e); + }); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 deleted file mode 100644 index c7d9447d8d..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 +++ /dev/null @@ -1,142 +0,0 @@ -import { isEmpty } from "@ember/utils"; -import { and, not } from "@ember/object/computed"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { ajax } from "discourse/lib/ajax"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4]; - -const SCSS_VARIABLE_NAMES = [ - // common/foundation/colors.scss - "primary", - "secondary", - "tertiary", - "quaternary", - "header_background", - "header_primary", - "highlight", - "danger", - "success", - "love", - // common/foundation/math.scss - "E", - "PI", - "LN2", - "SQRT2", - // common/foundation/variables.scss - "small-width", - "medium-width", - "large-width", - "google", - "instagram", - "facebook", - "cas", - "twitter", - "github", - "base-font-size", - "base-line-height", - "base-font-family", - "primary-low", - "primary-medium", - "secondary-low", - "secondary-medium", - "tertiary-low", - "quaternary-low", - "highlight-low", - "highlight-medium", - "danger-low", - "danger-medium", - "success-low", - "love-low" -]; - -export default Controller.extend(ModalFunctionality, { - adminCustomizeThemesShow: inject(), - - uploadUrl: "/admin/themes/upload_asset", - - onShow() { - this.set("name", null); - this.set("fileSelected", false); - }, - - enabled: and("nameValid", "fileSelected"), - disabled: not("enabled"), - - @discourseComputed("name", "adminCustomizeThemesShow.model.theme_fields") - errorMessage(name, themeFields) { - if (name) { - if (!name.match(/^[a-z_][a-z0-9_-]*$/i)) { - return I18n.t( - "admin.customize.theme.variable_name_error.invalid_syntax" - ); - } else if (SCSS_VARIABLE_NAMES.includes(name.toLowerCase())) { - return I18n.t("admin.customize.theme.variable_name_error.no_overwrite"); - } else if ( - themeFields.some( - tf => - THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && - name === tf.name - ) - ) { - return I18n.t( - "admin.customize.theme.variable_name_error.must_be_unique" - ); - } - } - - return null; - }, - - @discourseComputed("errorMessage") - nameValid(errorMessage) { - return null === errorMessage; - }, - - @observes("name") - uploadChanged() { - const file = $("#file-input")[0]; - this.set("fileSelected", file && file.files[0]); - }, - - actions: { - updateName() { - let name = this.name; - if (isEmpty(name)) { - name = $("#file-input")[0].files[0].name; - this.set("name", name.split(".")[0]); - } - this.uploadChanged(); - }, - - upload() { - const file = $("#file-input")[0].files[0]; - - const options = { - type: "POST", - processData: false, - contentType: false, - data: new FormData() - }; - - options.data.append("file", file); - - ajax(this.uploadUrl, options) - .then(result => { - const upload = { - upload_id: result.upload_id, - name: this.name, - original_filename: file.name - }; - this.adminCustomizeThemesShow.send("addUpload", upload); - this.send("closeModal"); - }) - .catch(e => { - popupAjaxError(e); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js b/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js new file mode 100644 index 0000000000..1ea601202d --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js @@ -0,0 +1,27 @@ +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; + +export default Controller.extend(ModalFunctionality, { + adminCustomizeColors: controller(), + + selectedBaseThemeId: null, + + init() { + this._super(...arguments); + + const defaultScheme = this.get( + "adminCustomizeColors.baseColorSchemes.0.base_scheme_id" + ); + this.set("selectedBaseThemeId", defaultScheme); + }, + + actions: { + selectBase() { + this.adminCustomizeColors.send( + "newColorSchemeWithBase", + this.selectedBaseThemeId + ); + this.send("closeModal"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 deleted file mode 100644 index e0b7bfe7e6..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-color-scheme-select-base.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; - -export default Controller.extend(ModalFunctionality, { - adminCustomizeColors: inject(), - - selectedBaseThemeId: null, - - init() { - this._super(...arguments); - - const defaultScheme = this.get( - "adminCustomizeColors.baseColorSchemes.0.base_scheme_id" - ); - this.set("selectedBaseThemeId", defaultScheme); - }, - - actions: { - selectBase() { - this.adminCustomizeColors.send( - "newColorSchemeWithBase", - this.selectedBaseThemeId - ); - this.send("closeModal"); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js new file mode 100644 index 0000000000..f1d1274abd --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js @@ -0,0 +1,83 @@ +import Controller from "@ember/controller"; +import { A } from "@ember/array"; +import { ajax } from "discourse/lib/ajax"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { observes } from "discourse-common/utils/decorators"; + +export default Controller.extend(ModalFunctionality, { + @observes("model") + modelChanged() { + const model = this.model; + const copy = A(); + const store = this.store; + + if (model) { + model.forEach(o => + copy.pushObject(store.createRecord("badge-grouping", o)) + ); + } + + this.set("workingCopy", copy); + }, + + moveItem(item, delta) { + const copy = this.workingCopy; + const index = copy.indexOf(item); + if (index + delta < 0 || index + delta >= copy.length) { + return; + } + + copy.removeAt(index); + copy.insertAt(index + delta, item); + }, + + actions: { + up(item) { + this.moveItem(item, -1); + }, + down(item) { + this.moveItem(item, 1); + }, + delete(item) { + this.workingCopy.removeObject(item); + }, + cancel() { + this.setProperties({ model: null, workingCopy: null }); + this.send("closeModal"); + }, + edit(item) { + item.set("editing", true); + }, + save(item) { + item.set("editing", false); + }, + add() { + const obj = this.store.createRecord("badge-grouping", { + editing: true, + name: I18n.t("admin.badges.badge_grouping") + }); + this.workingCopy.pushObject(obj); + }, + saveAll() { + let items = this.workingCopy; + const groupIds = items.map(i => i.get("id") || -1); + const names = items.map(i => i.get("name")); + + ajax("/admin/badges/badge_groupings", { + data: { ids: groupIds, names }, + type: "POST" + }).then( + data => { + items = this.model; + items.clear(); + data.badge_groupings.forEach(g => { + items.pushObject(this.store.createRecord("badge-grouping", g)); + }); + this.setProperties({ model: null, workingCopy: null }); + this.send("closeModal"); + }, + () => bootbox.alert(I18n.t("generic_error")) + ); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 deleted file mode 100644 index 1629aab70b..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 +++ /dev/null @@ -1,82 +0,0 @@ -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { observes } from "discourse-common/utils/decorators"; - -export default Controller.extend(ModalFunctionality, { - @observes("model") - modelChanged() { - const model = this.model; - const copy = Ember.A(); - const store = this.store; - - if (model) { - model.forEach(o => - copy.pushObject(store.createRecord("badge-grouping", o)) - ); - } - - this.set("workingCopy", copy); - }, - - moveItem(item, delta) { - const copy = this.workingCopy; - const index = copy.indexOf(item); - if (index + delta < 0 || index + delta >= copy.length) { - return; - } - - copy.removeAt(index); - copy.insertAt(index + delta, item); - }, - - actions: { - up(item) { - this.moveItem(item, -1); - }, - down(item) { - this.moveItem(item, 1); - }, - delete(item) { - this.workingCopy.removeObject(item); - }, - cancel() { - this.setProperties({ model: null, workingCopy: null }); - this.send("closeModal"); - }, - edit(item) { - item.set("editing", true); - }, - save(item) { - item.set("editing", false); - }, - add() { - const obj = this.store.createRecord("badge-grouping", { - editing: true, - name: I18n.t("admin.badges.badge_grouping") - }); - this.workingCopy.pushObject(obj); - }, - saveAll() { - let items = this.workingCopy; - const groupIds = items.map(i => i.get("id") || -1); - const names = items.map(i => i.get("name")); - - ajax("/admin/badges/badge_groupings", { - data: { ids: groupIds, names }, - method: "POST" - }).then( - data => { - items = this.model; - items.clear(); - data.badge_groupings.forEach(g => { - items.pushObject(this.store.createRecord("badge-grouping", g)); - }); - this.setProperties({ model: null, workingCopy: null }); - this.send("closeModal"); - }, - () => bootbox.alert(I18n.t("generic_error")) - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-incoming-email.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js b/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js new file mode 100644 index 0000000000..16cf2034d5 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js @@ -0,0 +1,198 @@ +import { equal, match, alias } from "@ember/object/computed"; +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import { THEMES, COMPONENTS } from "admin/models/theme"; +import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes"; +import { set } from "@ember/object"; + +const MIN_NAME_LENGTH = 4; + +export default Controller.extend(ModalFunctionality, { + adminCustomizeThemes: controller(), + themesController: controller("adminCustomizeThemes"), + popular: equal("selection", "popular"), + local: equal("selection", "local"), + remote: equal("selection", "remote"), + create: equal("selection", "create"), + selection: "popular", + loading: false, + keyGenUrl: "/admin/themes/generate_key_pair", + importUrl: "/admin/themes/import", + recordType: "theme", + checkPrivate: match("uploadUrl", /^git/), + localFile: null, + uploadUrl: null, + urlPlaceholder: "https://github.com/discourse/sample_theme", + advancedVisible: false, + selectedType: alias("themesController.currentTab"), + component: equal("selectedType", COMPONENTS), + + init() { + this._super(...arguments); + + this.createTypes = [ + { name: I18n.t("admin.customize.theme.theme"), value: THEMES }, + { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS } + ]; + }, + + @discourseComputed("themesController.installedThemes") + themes(installedThemes) { + return POPULAR_THEMES.map(t => { + if (installedThemes.includes(t.name)) { + set(t, "installed", true); + } + return t; + }); + }, + + @discourseComputed( + "loading", + "remote", + "uploadUrl", + "local", + "localFile", + "create", + "nameTooShort" + ) + installDisabled( + isLoading, + isRemote, + uploadUrl, + isLocal, + localFile, + isCreate, + nameTooShort + ) { + return ( + isLoading || + (isRemote && !uploadUrl) || + (isLocal && !localFile) || + (isCreate && nameTooShort) + ); + }, + + @observes("privateChecked") + privateWasChecked() { + this.privateChecked + ? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git") + : this.set("urlPlaceholder", "https://github.com/discourse/sample_theme"); + + const checked = this.privateChecked; + if (checked && !this._keyLoading) { + this._keyLoading = true; + ajax(this.keyGenUrl, { type: "POST" }) + .then(pair => { + this.setProperties({ + privateKey: pair.private_key, + publicKey: pair.public_key + }); + }) + .catch(popupAjaxError) + .finally(() => { + this._keyLoading = false; + }); + } + }, + + @discourseComputed("name") + nameTooShort(name) { + return !name || name.length < MIN_NAME_LENGTH; + }, + + @discourseComputed("component") + placeholder(component) { + if (component) { + return I18n.t("admin.customize.theme.component_name"); + } else { + return I18n.t("admin.customize.theme.theme_name"); + } + }, + + @discourseComputed("selection") + submitLabel(selection) { + return `admin.customize.theme.${ + selection === "create" ? "create" : "install" + }`; + }, + + @discourseComputed("privateChecked", "checkPrivate", "publicKey") + showPublicKey(privateChecked, checkPrivate, publicKey) { + return privateChecked && checkPrivate && publicKey; + }, + + actions: { + uploadLocaleFile() { + this.set("localFile", $("#file-input")[0].files[0]); + }, + + toggleAdvanced() { + this.toggleProperty("advancedVisible"); + }, + + installThemeFromList(url) { + this.set("uploadUrl", url); + this.send("installTheme"); + }, + + installTheme() { + if (this.create) { + this.set("loading", true); + const theme = this.store.createRecord(this.recordType); + theme + .save({ name: this.name, component: this.component }) + .then(() => { + this.themesController.send("addTheme", theme); + this.send("closeModal"); + }) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + + return; + } + + let options = { + type: "POST" + }; + + if (this.local) { + options.processData = false; + options.contentType = false; + options.data = new FormData(); + options.data.append("theme", this.localFile); + } + + if (this.remote || this.popular) { + options.data = { + remote: this.uploadUrl, + branch: this.branch + }; + + if (this.privateChecked) { + options.data.private_key = this.privateKey; + } + } + + if (this.get("model.user_id")) { + // Used by theme-creator + options.data["user_id"] = this.get("model.user_id"); + } + + this.set("loading", true); + ajax(this.importUrl, options) + .then(result => { + const theme = this.store.createRecord(this.recordType, result.theme); + this.adminCustomizeThemes.send("addTheme", theme); + this.send("closeModal"); + }) + .then(() => { + this.setProperties({ privateKey: null, publicKey: null }); + }) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js.es6 deleted file mode 100644 index cfb61ccc8f..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js.es6 +++ /dev/null @@ -1,199 +0,0 @@ -import { equal, match, alias } from "@ember/object/computed"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { THEMES, COMPONENTS } from "admin/models/theme"; -import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes"; -import { set } from "@ember/object"; - -const MIN_NAME_LENGTH = 4; - -export default Controller.extend(ModalFunctionality, { - popular: equal("selection", "popular"), - local: equal("selection", "local"), - remote: equal("selection", "remote"), - create: equal("selection", "create"), - selection: "popular", - adminCustomizeThemes: inject(), - loading: false, - keyGenUrl: "/admin/themes/generate_key_pair", - importUrl: "/admin/themes/import", - recordType: "theme", - checkPrivate: match("uploadUrl", /^git/), - localFile: null, - uploadUrl: null, - urlPlaceholder: "https://github.com/discourse/sample_theme", - advancedVisible: false, - themesController: inject("adminCustomizeThemes"), - selectedType: alias("themesController.currentTab"), - component: equal("selectedType", COMPONENTS), - - init() { - this._super(...arguments); - - this.createTypes = [ - { name: I18n.t("admin.customize.theme.theme"), value: THEMES }, - { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS } - ]; - }, - - @discourseComputed("themesController.installedThemes") - themes(installedThemes) { - return POPULAR_THEMES.map(t => { - if (installedThemes.includes(t.name)) { - set(t, "installed", true); - } - return t; - }); - }, - - @discourseComputed( - "loading", - "remote", - "uploadUrl", - "local", - "localFile", - "create", - "nameTooShort" - ) - installDisabled( - isLoading, - isRemote, - uploadUrl, - isLocal, - localFile, - isCreate, - nameTooShort - ) { - return ( - isLoading || - (isRemote && !uploadUrl) || - (isLocal && !localFile) || - (isCreate && nameTooShort) - ); - }, - - @observes("privateChecked") - privateWasChecked() { - this.privateChecked - ? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git") - : this.set("urlPlaceholder", "https://github.com/discourse/sample_theme"); - - const checked = this.privateChecked; - if (checked && !this._keyLoading) { - this._keyLoading = true; - ajax(this.keyGenUrl, { method: "POST" }) - .then(pair => { - this.setProperties({ - privateKey: pair.private_key, - publicKey: pair.public_key - }); - }) - .catch(popupAjaxError) - .finally(() => { - this._keyLoading = false; - }); - } - }, - - @discourseComputed("name") - nameTooShort(name) { - return !name || name.length < MIN_NAME_LENGTH; - }, - - @discourseComputed("component") - placeholder(component) { - if (component) { - return I18n.t("admin.customize.theme.component_name"); - } else { - return I18n.t("admin.customize.theme.theme_name"); - } - }, - - @discourseComputed("selection") - submitLabel(selection) { - return `admin.customize.theme.${ - selection === "create" ? "create" : "install" - }`; - }, - - @discourseComputed("privateChecked", "checkPrivate", "publicKey") - showPublicKey(privateChecked, checkPrivate, publicKey) { - return privateChecked && checkPrivate && publicKey; - }, - - actions: { - uploadLocaleFile() { - this.set("localFile", $("#file-input")[0].files[0]); - }, - - toggleAdvanced() { - this.toggleProperty("advancedVisible"); - }, - - installThemeFromList(url) { - this.set("uploadUrl", url); - this.send("installTheme"); - }, - - installTheme() { - if (this.create) { - this.set("loading", true); - const theme = this.store.createRecord(this.recordType); - theme - .save({ name: this.name, component: this.component }) - .then(() => { - this.themesController.send("addTheme", theme); - this.send("closeModal"); - }) - .catch(popupAjaxError) - .finally(() => this.set("loading", false)); - - return; - } - - let options = { - type: "POST" - }; - - if (this.local) { - options.processData = false; - options.contentType = false; - options.data = new FormData(); - options.data.append("theme", this.localFile); - } - - if (this.remote || this.popular) { - options.data = { - remote: this.uploadUrl, - branch: this.branch - }; - - if (this.privateChecked) { - options.data.private_key = this.privateKey; - } - } - - if (this.get("model.user_id")) { - // Used by theme-creator - options.data["user_id"] = this.get("model.user_id"); - } - - this.set("loading", true); - ajax(this.importUrl, options) - .then(result => { - const theme = this.store.createRecord(this.recordType, result.theme); - this.adminCustomizeThemes.send("addTheme", theme); - this.send("closeModal"); - }) - .then(() => { - this.setProperties({ privateKey: null, publicKey: null }); - }) - .catch(popupAjaxError) - .finally(() => this.set("loading", false)); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js new file mode 100644 index 0000000000..2195c29631 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js @@ -0,0 +1,35 @@ +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; + +export default Controller.extend(ModalFunctionality, { + adminUserIndex: controller(), + username: alias("model.username"), + targetUsername: alias("model.targetUsername"), + + onShow() { + this.set("value", null); + }, + + @discourseComputed("username", "targetUsername") + text(username, targetUsername) { + return `transfer @${username} to @${targetUsername}`; + }, + + @discourseComputed("value", "text") + mergeDisabled(value, text) { + return !value || text !== value; + }, + + actions: { + merge() { + this.adminUserIndex.send("merge", this.targetUsername); + this.send("closeModal"); + }, + + cancel() { + this.send("closeModal"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js new file mode 100644 index 0000000000..535870cd6f --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js @@ -0,0 +1,29 @@ +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; + +export default Controller.extend(ModalFunctionality, { + adminUserIndex: controller(), + username: alias("model.username"), + + onShow() { + this.set("targetUsername", null); + }, + + @discourseComputed("username", "targetUsername") + mergeDisabled(username, targetUsername) { + return !targetUsername || username === targetUsername; + }, + + actions: { + merge() { + this.send("closeModal"); + this.adminUserIndex.send("showMergeConfirmation", this.targetUsername); + }, + + cancel() { + this.send("closeModal"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-reseed.js b/app/assets/javascripts/admin/controllers/modals/admin-reseed.js new file mode 100644 index 0000000000..326a3a66a7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-reseed.js @@ -0,0 +1,43 @@ +import Controller from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { ajax } from "discourse/lib/ajax"; + +export default Controller.extend(ModalFunctionality, { + loading: true, + reseeding: false, + categories: null, + topics: null, + + onShow() { + ajax("/admin/customize/reseed") + .then(result => { + this.setProperties({ + categories: result.categories, + topics: result.topics + }); + }) + .finally(() => this.set("loading", false)); + }, + + _extractSelectedIds(items) { + return items.filter(item => item.selected).map(item => item.id); + }, + + actions: { + reseed() { + this.set("reseeding", true); + ajax("/admin/customize/reseed", { + data: { + category_ids: this._extractSelectedIds(this.categories), + topic_ids: this._extractSelectedIds(this.topics) + }, + type: "POST" + }) + .then( + () => this.send("closeModal"), + () => bootbox.alert(I18n.t("generic_error")) + ) + .finally(() => this.set("reseeding", false)); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-reseed.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-reseed.js.es6 deleted file mode 100644 index 176c46be36..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-reseed.js.es6 +++ /dev/null @@ -1,43 +0,0 @@ -import Controller from "@ember/controller"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { ajax } from "discourse/lib/ajax"; - -export default Controller.extend(ModalFunctionality, { - loading: true, - reseeding: false, - categories: null, - topics: null, - - onShow() { - ajax("/admin/customize/reseed") - .then(result => { - this.setProperties({ - categories: result.categories, - topics: result.topics - }); - }) - .finally(() => this.set("loading", false)); - }, - - _extractSelectedIds(items) { - return items.filter(item => item.selected).map(item => item.id); - }, - - actions: { - reseed() { - this.set("reseeding", true); - ajax("/admin/customize/reseed", { - data: { - category_ids: this._extractSelectedIds(this.categories), - topic_ids: this._extractSelectedIds(this.topics) - }, - method: "POST" - }) - .then( - () => this.send("closeModal"), - () => bootbox.alert(I18n.t("generic_error")) - ) - .finally(() => this.set("reseeding", false)); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-silence-user.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js new file mode 100644 index 0000000000..c2136e6cc6 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js @@ -0,0 +1,22 @@ +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; + +export default Controller.extend(ModalFunctionality, { + adminBackupsLogs: controller(), + + actions: { + startBackupWithUploads() { + this.send("closeModal"); + this.send("startBackup", true); + }, + + startBackupWithoutUploads() { + this.send("closeModal"); + this.send("startBackup", false); + }, + + cancel() { + this.send("closeModal"); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 deleted file mode 100644 index b4cc2f188d..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; - -export default Controller.extend(ModalFunctionality, { - adminBackupsLogs: inject(), - - actions: { - startBackupWithUploads() { - this.send("closeModal"); - this.send("startBackup", true); - }, - - startBackupWithoutUploads() { - this.send("closeModal"); - this.send("startBackup", false); - }, - - cancel() { - this.send("closeModal"); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-theme-change.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-theme-change.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-theme-change.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-theme-change.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-uploaded-image-list.js diff --git a/app/assets/javascripts/admin/controllers/modals/admin-watched-word-test.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-watched-word-test.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/admin-watched-word-test.js.es6 rename to app/assets/javascripts/admin/controllers/modals/admin-watched-word-test.js diff --git a/app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js.es6 b/app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js similarity index 100% rename from app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js.es6 rename to app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js diff --git a/app/assets/javascripts/admin/helpers/check-icon.js.es6 b/app/assets/javascripts/admin/helpers/check-icon.js similarity index 100% rename from app/assets/javascripts/admin/helpers/check-icon.js.es6 rename to app/assets/javascripts/admin/helpers/check-icon.js diff --git a/app/assets/javascripts/admin/helpers/disposition-icon.js.es6 b/app/assets/javascripts/admin/helpers/disposition-icon.js similarity index 100% rename from app/assets/javascripts/admin/helpers/disposition-icon.js.es6 rename to app/assets/javascripts/admin/helpers/disposition-icon.js diff --git a/app/assets/javascripts/admin/helpers/human-size.js.es6 b/app/assets/javascripts/admin/helpers/human-size.js similarity index 100% rename from app/assets/javascripts/admin/helpers/human-size.js.es6 rename to app/assets/javascripts/admin/helpers/human-size.js diff --git a/app/assets/javascripts/admin/helpers/post-action-title.js.es6 b/app/assets/javascripts/admin/helpers/post-action-title.js similarity index 100% rename from app/assets/javascripts/admin/helpers/post-action-title.js.es6 rename to app/assets/javascripts/admin/helpers/post-action-title.js diff --git a/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 b/app/assets/javascripts/admin/helpers/preserve-newlines.js similarity index 100% rename from app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 rename to app/assets/javascripts/admin/helpers/preserve-newlines.js diff --git a/app/assets/javascripts/admin/helpers/value-at-tl.js.es6 b/app/assets/javascripts/admin/helpers/value-at-tl.js similarity index 100% rename from app/assets/javascripts/admin/helpers/value-at-tl.js.es6 rename to app/assets/javascripts/admin/helpers/value-at-tl.js diff --git a/app/assets/javascripts/admin/mixins/penalty-controller.js b/app/assets/javascripts/admin/mixins/penalty-controller.js new file mode 100644 index 0000000000..46ba124e18 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/penalty-controller.js @@ -0,0 +1,69 @@ +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import Mixin from "@ember/object/mixin"; +import { next } from "@ember/runloop"; +import { Promise } from "rsvp"; + +export default Mixin.create(ModalFunctionality, { + reason: null, + message: null, + postEdit: null, + postAction: null, + user: null, + postId: null, + successCallback: null, + confirmClose: false, + + resetModal() { + this.setProperties({ + reason: null, + message: null, + loadingUser: true, + postId: null, + postEdit: null, + postAction: "delete", + before: null, + successCallback: null, + confirmClose: false + }); + }, + + beforeClose() { + // prompt a confirmation if we have unsaved content + if ( + !this.confirmClose && + ((this.reason && this.reason.length > 1) || + (this.message && this.message.length > 1)) + ) { + this.send("hideModal"); + bootbox.confirm(I18n.t("admin.user.confirm_cancel_penalty"), result => { + if (result) { + next(() => { + this.set("confirmClose", true); + this.send("closeModal"); + }); + } else { + next(() => this.send("reopenModal")); + } + }); + return false; + } + }, + + penalize(cb) { + let before = this.before; + let promise = before ? before() : Promise.resolve(); + + return promise + .then(() => cb()) + .then(result => { + this.set("confirmClose", true); + this.send("closeModal"); + let callback = this.successCallback; + if (callback) { + callback(result); + } + }) + .catch(popupAjaxError); + } +}); diff --git a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 deleted file mode 100644 index cb07e9d2ca..0000000000 --- a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 +++ /dev/null @@ -1,43 +0,0 @@ -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import Mixin from "@ember/object/mixin"; -import { Promise } from "rsvp"; - -export default Mixin.create(ModalFunctionality, { - reason: null, - message: null, - postEdit: null, - postAction: null, - user: null, - postId: null, - successCallback: null, - - resetModal() { - this.setProperties({ - reason: null, - message: null, - loadingUser: true, - postId: null, - postEdit: null, - postAction: "delete", - before: null, - successCallback: null - }); - }, - - penalize(cb) { - let before = this.before; - let promise = before ? before() : Promise.resolve(); - - return promise - .then(() => cb()) - .then(result => { - this.send("closeModal"); - let callback = this.successCallback; - if (callback) { - callback(result); - } - }) - .catch(popupAjaxError); - } -}); diff --git a/app/assets/javascripts/admin/mixins/period-computation.js.es6 b/app/assets/javascripts/admin/mixins/period-computation.js similarity index 100% rename from app/assets/javascripts/admin/mixins/period-computation.js.es6 rename to app/assets/javascripts/admin/mixins/period-computation.js diff --git a/app/assets/javascripts/admin/mixins/setting-component.js b/app/assets/javascripts/admin/mixins/setting-component.js new file mode 100644 index 0000000000..200ec00c1f --- /dev/null +++ b/app/assets/javascripts/admin/mixins/setting-component.js @@ -0,0 +1,254 @@ +import { warn } from "@ember/debug"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias, oneWay } from "@ember/object/computed"; +import { categoryLinkHTML } from "discourse/helpers/category-link"; +import { on } from "@ember/object/evented"; +import Mixin from "@ember/object/mixin"; +import showModal from "discourse/lib/show-modal"; +import { Promise } from "rsvp"; +import { ajax } from "discourse/lib/ajax"; + +const CUSTOM_TYPES = [ + "bool", + "enum", + "list", + "url_list", + "host_list", + "category_list", + "value_list", + "category", + "uploaded_image_list", + "compact_list", + "secret_list", + "upload", + "group_list", + "tag_list", + "color" +]; + +const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"]; + +function splitPipes(str) { + if (typeof str === "string") { + return str.split("|").filter(Boolean); + } else { + return []; + } +} + +export default Mixin.create({ + classNameBindings: [":row", ":setting", "overridden", "typeClass"], + content: alias("setting"), + validationMessage: null, + isSecret: oneWay("setting.secret"), + + @discourseComputed("buffered.value", "setting.value") + dirty(bufferVal, settingVal) { + if (bufferVal === null || bufferVal === undefined) bufferVal = ""; + if (settingVal === null || settingVal === undefined) settingVal = ""; + + return bufferVal.toString() !== settingVal.toString(); + }, + + @discourseComputed("setting", "buffered.value") + preview(setting, value) { + // A bit hacky, but allows us to use helpers + if (setting.get("setting") === "category_style") { + let category = this.site.get("categories.firstObject"); + if (category) { + return categoryLinkHTML(category, { + categoryStyle: value + }); + } + } + let preview = setting.get("preview"); + if (preview) { + return new Handlebars.SafeString( + "
" + + preview.replace(/\{\{value\}\}/g, value) + + "
" + ); + } + }, + + @discourseComputed("componentType") + typeClass(componentType) { + return componentType.replace(/\_/g, "-"); + }, + + @discourseComputed("setting.setting", "setting.label") + settingName(setting, label) { + return label || setting.replace(/\_/g, " "); + }, + + @discourseComputed("type") + componentType(type) { + return CUSTOM_TYPES.indexOf(type) !== -1 ? type : "string"; + }, + + @discourseComputed("setting") + type(setting) { + if (setting.type === "list" && setting.list_type) { + return `${setting.list_type}_list`; + } + + return setting.type; + }, + + @discourseComputed("typeClass") + componentName(typeClass) { + return "site-settings/" + typeClass; + }, + + @discourseComputed("setting.anyValue") + allowAny(anyValue) { + return anyValue !== false; + }, + + @discourseComputed("setting.default", "buffered.value") + overridden(settingDefault, bufferedValue) { + return settingDefault !== bufferedValue; + }, + + @discourseComputed("buffered.value") + bufferedValues: splitPipes, + + @discourseComputed("setting.defaultValues") + defaultValues: splitPipes, + + @discourseComputed("defaultValues", "bufferedValues") + defaultIsAvailable(defaultValues, bufferedValues) { + return ( + defaultValues && + defaultValues.length > 0 && + !defaultValues.every(value => bufferedValues.includes(value)) + ); + }, + + _watchEnterKey: on("didInsertElement", function() { + $(this.element).on("keydown.setting-enter", ".input-setting-string", e => { + if (e.keyCode === 13) { + // enter key + this.send("save"); + } + }); + }), + + _removeBindings: on("willDestroyElement", function() { + $(this.element).off("keydown.setting-enter"); + }), + + _save() { + warn("You should define a `_save` method", { + id: "discourse.setting-component.missing-save" + }); + return Promise.resolve(); + }, + + actions: { + update() { + const defaultUserPreferences = [ + "default_email_digest_frequency", + "default_include_tl0_in_digests", + "default_email_level", + "default_email_messages_level", + "default_email_mailing_list_mode", + "default_email_mailing_list_mode_frequency", + "default_email_previous_replies", + "default_email_in_reply_to", + "default_other_new_topic_duration_minutes", + "default_other_auto_track_topics_after_msecs", + "default_other_notification_level_when_replying", + "default_other_external_links_in_new_tab", + "default_other_enable_quoting", + "default_other_enable_defer", + "default_other_dynamic_favicon", + "default_other_like_notification_frequency", + "default_topics_automatic_unpin", + "default_categories_watching", + "default_categories_tracking", + "default_categories_muted", + "default_categories_watching_first_post", + "default_tags_watching", + "default_tags_tracking", + "default_tags_muted", + "default_tags_watching_first_post", + "default_text_size", + "default_title_count_mode" + ]; + const key = this.buffered.get("setting"); + + if (defaultUserPreferences.includes(key)) { + const data = {}; + data[key] = this.buffered.get("value"); + + ajax(`/admin/site_settings/${key}/user_count.json`, { + type: "PUT", + data + }).then(result => { + const count = result.user_count; + + if (count > 0) { + const controller = showModal("site-setting-default-categories", { + model: { + count: result.user_count, + key: key.replace(/_/g, " ") + }, + admin: true + }); + + controller.set("onClose", () => { + this.updateExistingUsers = controller.updateExistingUsers; + this.send("save"); + }); + } else { + this.send("save"); + } + }); + } else { + this.send("save"); + } + }, + + save() { + this._save() + .then(() => { + this.set("validationMessage", null); + this.commitBuffer(); + if (AUTO_REFRESH_ON_SAVE.includes(this.setting.setting)) { + this.afterSave(); + } + }) + .catch(e => { + if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { + this.set("validationMessage", e.jqXHR.responseJSON.errors[0]); + } else { + this.set("validationMessage", I18n.t("generic_error")); + } + }); + }, + + cancel() { + this.rollbackBuffer(); + }, + + resetDefault() { + this.set("buffered.value", this.get("setting.default")); + }, + + toggleSecret() { + this.toggleProperty("isSecret"); + }, + + setDefaultValues() { + this.set( + "buffered.value", + this.bufferedValues + .concat(this.defaultValues) + .uniq() + .join("|") + ); + return false; + } + } +}); diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 deleted file mode 100644 index c182abadd6..0000000000 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ /dev/null @@ -1,252 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { alias, oneWay } from "@ember/object/computed"; -import { categoryLinkHTML } from "discourse/helpers/category-link"; -import { on } from "@ember/object/evented"; -import Mixin from "@ember/object/mixin"; -import showModal from "discourse/lib/show-modal"; -import { Promise } from "rsvp"; -import { ajax } from "discourse/lib/ajax"; - -const CUSTOM_TYPES = [ - "bool", - "enum", - "list", - "url_list", - "host_list", - "category_list", - "value_list", - "category", - "uploaded_image_list", - "compact_list", - "secret_list", - "upload", - "group_list", - "tag_list" -]; - -const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"]; - -function splitPipes(str) { - if (typeof str === "string") { - return str.split("|").filter(Boolean); - } else { - return []; - } -} - -export default Mixin.create({ - classNameBindings: [":row", ":setting", "overridden", "typeClass"], - content: alias("setting"), - validationMessage: null, - isSecret: oneWay("setting.secret"), - - @discourseComputed("buffered.value", "setting.value") - dirty(bufferVal, settingVal) { - if (bufferVal === null || bufferVal === undefined) bufferVal = ""; - if (settingVal === null || settingVal === undefined) settingVal = ""; - - return bufferVal.toString() !== settingVal.toString(); - }, - - @discourseComputed("setting", "buffered.value") - preview(setting, value) { - // A bit hacky, but allows us to use helpers - if (setting.get("setting") === "category_style") { - let category = this.site.get("categories.firstObject"); - if (category) { - return categoryLinkHTML(category, { - categoryStyle: value - }); - } - } - let preview = setting.get("preview"); - if (preview) { - return new Handlebars.SafeString( - "
" + - preview.replace(/\{\{value\}\}/g, value) + - "
" - ); - } - }, - - @discourseComputed("componentType") - typeClass(componentType) { - return componentType.replace(/\_/g, "-"); - }, - - @discourseComputed("setting.setting", "setting.label") - settingName(setting, label) { - return label || setting.replace(/\_/g, " "); - }, - - @discourseComputed("type") - componentType(type) { - return CUSTOM_TYPES.indexOf(type) !== -1 ? type : "string"; - }, - - @discourseComputed("setting") - type(setting) { - if (setting.type === "list" && setting.list_type) { - return `${setting.list_type}_list`; - } - - return setting.type; - }, - - @discourseComputed("typeClass") - componentName(typeClass) { - return "site-settings/" + typeClass; - }, - - @discourseComputed("setting.anyValue") - allowAny(anyValue) { - return anyValue !== false; - }, - - @discourseComputed("setting.default", "buffered.value") - overridden(settingDefault, bufferedValue) { - return settingDefault !== bufferedValue; - }, - - @discourseComputed("buffered.value") - bufferedValues: splitPipes, - - @discourseComputed("setting.defaultValues") - defaultValues: splitPipes, - - @discourseComputed("defaultValues", "bufferedValues") - defaultIsAvailable(defaultValues, bufferedValues) { - return ( - defaultValues && - defaultValues.length > 0 && - !defaultValues.every(value => bufferedValues.includes(value)) - ); - }, - - _watchEnterKey: on("didInsertElement", function() { - $(this.element).on("keydown.setting-enter", ".input-setting-string", e => { - if (e.keyCode === 13) { - // enter key - this.send("save"); - } - }); - }), - - _removeBindings: on("willDestroyElement", function() { - $(this.element).off("keydown.setting-enter"); - }), - - _save() { - Ember.warn("You should define a `_save` method", { - id: "discourse.setting-component.missing-save" - }); - return Promise.resolve(); - }, - - actions: { - update() { - const defaultUserPreferences = [ - "default_email_digest_frequency", - "default_include_tl0_in_digests", - "default_email_level", - "default_email_messages_level", - "default_email_mailing_list_mode", - "default_email_mailing_list_mode_frequency", - "default_email_previous_replies", - "default_email_in_reply_to", - "default_other_new_topic_duration_minutes", - "default_other_auto_track_topics_after_msecs", - "default_other_notification_level_when_replying", - "default_other_external_links_in_new_tab", - "default_other_enable_quoting", - "default_other_enable_defer", - "default_other_dynamic_favicon", - "default_other_like_notification_frequency", - "default_topics_automatic_unpin", - "default_categories_watching", - "default_categories_tracking", - "default_categories_muted", - "default_categories_watching_first_post", - "default_tags_watching", - "default_tags_tracking", - "default_tags_muted", - "default_tags_watching_first_post", - "default_text_size", - "default_title_count_mode" - ]; - const key = this.buffered.get("setting"); - - if (defaultUserPreferences.includes(key)) { - const data = {}; - data[key] = this.buffered.get("value"); - - ajax(`/admin/site_settings/${key}/user_count.json`, { - type: "PUT", - data - }).then(result => { - const count = result.user_count; - - if (count > 0) { - const controller = showModal("site-setting-default-categories", { - model: { - count: result.user_count, - key: key.replace(/_/g, " ") - }, - admin: true - }); - - controller.set("onClose", () => { - this.updateExistingUsers = controller.updateExistingUsers; - this.send("save"); - }); - } else { - this.send("save"); - } - }); - } else { - this.send("save"); - } - }, - - save() { - this._save() - .then(() => { - this.set("validationMessage", null); - this.commitBuffer(); - if (AUTO_REFRESH_ON_SAVE.includes(this.setting.setting)) { - this.afterSave(); - } - }) - .catch(e => { - if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - this.set("validationMessage", e.jqXHR.responseJSON.errors[0]); - } else { - this.set("validationMessage", I18n.t("generic_error")); - } - }); - }, - - cancel() { - this.rollbackBuffer(); - }, - - resetDefault() { - this.set("buffered.value", this.get("setting.default")); - }, - - toggleSecret() { - this.toggleProperty("isSecret"); - }, - - setDefaultValues() { - this.set( - "buffered.value", - this.bufferedValues - .concat(this.defaultValues) - .uniq() - .join("|") - ); - return false; - } - } -}); diff --git a/app/assets/javascripts/admin/mixins/setting-object.js.es6 b/app/assets/javascripts/admin/mixins/setting-object.js similarity index 100% rename from app/assets/javascripts/admin/mixins/setting-object.js.es6 rename to app/assets/javascripts/admin/mixins/setting-object.js diff --git a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard.js similarity index 100% rename from app/assets/javascripts/admin/models/admin-dashboard.js.es6 rename to app/assets/javascripts/admin/models/admin-dashboard.js diff --git a/app/assets/javascripts/admin/models/admin-user.js b/app/assets/javascripts/admin/models/admin-user.js new file mode 100644 index 0000000000..454daf4b19 --- /dev/null +++ b/app/assets/javascripts/admin/models/admin-user.js @@ -0,0 +1,590 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { filter, or, gt, lt, not } from "@ember/object/computed"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { ajax } from "discourse/lib/ajax"; +import { propertyNotEqual } from "discourse/lib/computed"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import Group from "discourse/models/group"; +import { userPath } from "discourse/lib/url"; +import { Promise } from "rsvp"; +import User from "discourse/models/user"; + +const wrapAdmin = user => (user ? AdminUser.create(user) : null); + +const AdminUser = User.extend({ + adminUserView: true, + customGroups: filter("groups", g => !g.automatic && Group.create(g)), + automaticGroups: filter("groups", g => g.automatic && Group.create(g)), + + canViewProfile: or("active", "staged"), + + @discourseComputed("bounce_score", "reset_bounce_score_after") + bounceScore(bounce_score, reset_bounce_score_after) { + if (bounce_score > 0) { + return `${bounce_score} - ${moment(reset_bounce_score_after).format( + "LL" + )}`; + } else { + return bounce_score; + } + }, + + @discourseComputed("bounce_score") + bounceScoreExplanation(bounce_score) { + if (bounce_score === 0) { + return I18n.t("admin.user.bounce_score_explanation.none"); + } else if (bounce_score < Discourse.SiteSettings.bounce_score_threshold) { + return I18n.t("admin.user.bounce_score_explanation.some"); + } else { + return I18n.t("admin.user.bounce_score_explanation.threshold_reached"); + } + }, + + @discourseComputed + bounceLink() { + return Discourse.getURL("/admin/email/bounced"); + }, + + canResetBounceScore: gt("bounce_score", 0), + + resetBounceScore() { + return ajax(`/admin/users/${this.id}/reset_bounce_score`, { + type: "POST" + }).then(() => + this.setProperties({ + bounce_score: 0, + reset_bounce_score_after: null + }) + ); + }, + + groupAdded(added) { + return ajax(`/admin/users/${this.id}/groups`, { + type: "POST", + data: { group_id: added.id } + }).then(() => this.groups.pushObject(added)); + }, + + groupRemoved(groupId) { + return ajax(`/admin/users/${this.id}/groups/${groupId}`, { + type: "DELETE" + }).then(() => { + this.set("groups.[]", this.groups.rejectBy("id", groupId)); + if (this.primary_group_id === groupId) { + this.set("primary_group_id", null); + } + }); + }, + + revokeApiKey() { + return ajax(`/admin/users/${this.id}/revoke_api_key`, { + type: "DELETE" + }).then(() => this.set("api_key", null)); + }, + + deleteAllPosts() { + let deletedPosts = 0; + const user = this; + const message = I18n.messageFormat( + "admin.user.delete_all_posts_confirm_MF", + { + POSTS: user.get("post_count"), + TOPICS: user.get("topic_count") + } + ); + const buttons = [ + { + label: I18n.t("composer.cancel"), + class: "d-modal-cancel", + link: true + }, + { + label: + `${iconHTML("exclamation-triangle")} ` + + I18n.t("admin.user.delete_all_posts"), + class: "btn btn-danger", + callback: () => { + openProgressModal(); + performDelete(); + } + } + ]; + const openProgressModal = () => { + bootbox.dialog( + `

${I18n.t( + "admin.user.delete_posts_progress" + )}

`, + [], + { classes: "delete-posts-progress" } + ); + }; + const performDelete = () => { + let deletedPercentage = 0; + return ajax(`/admin/users/${user.get("id")}/delete_posts_batch`, { + type: "PUT" + }) + .then(({ posts_deleted }) => { + if (posts_deleted === 0) { + user.set("post_count", 0); + bootbox.hideAll(); + } else { + deletedPosts += posts_deleted; + deletedPercentage = Math.floor( + (deletedPosts * 100) / user.get("post_count") + ); + $(".delete-posts-progress .progress-bar > span").css({ + width: `${deletedPercentage}%` + }); + performDelete(); + } + }) + .catch(e => { + bootbox.hideAll(); + let error; + AdminUser.find(user.get("id")).then(u => user.setProperties(u)); + if (e.responseJSON && e.responseJSON.errors) { + error = e.responseJSON.errors[0]; + } + error = error || I18n.t("admin.user.delete_posts_failed"); + bootbox.alert(error); + }); + }; + + bootbox.dialog(message, buttons, { classes: "delete-all-posts" }); + }, + + revokeAdmin() { + return ajax(`/admin/users/${this.id}/revoke_admin`, { + type: "PUT" + }).then(() => { + this.setProperties({ + admin: false, + can_grant_admin: true, + can_revoke_admin: false + }); + }); + }, + + grantAdmin() { + return ajax(`/admin/users/${this.id}/grant_admin`, { + type: "PUT" + }) + .then(() => { + bootbox.alert(I18n.t("admin.user.grant_admin_confirm")); + }) + .catch(popupAjaxError); + }, + + revokeModeration() { + return ajax(`/admin/users/${this.id}/revoke_moderation`, { + type: "PUT" + }) + .then(() => { + this.setProperties({ + moderator: false, + can_grant_moderation: true, + can_revoke_moderation: false + }); + }) + .catch(popupAjaxError); + }, + + grantModeration() { + return ajax(`/admin/users/${this.id}/grant_moderation`, { + type: "PUT" + }) + .then(() => { + this.setProperties({ + moderator: true, + can_grant_moderation: false, + can_revoke_moderation: true + }); + }) + .catch(popupAjaxError); + }, + + disableSecondFactor() { + return ajax(`/admin/users/${this.id}/disable_second_factor`, { + type: "PUT" + }) + .then(() => { + this.set("second_factor_enabled", false); + }) + .catch(popupAjaxError); + }, + + approve(approvedBy) { + return ajax(`/admin/users/${this.id}/approve`, { + type: "PUT" + }).then(() => { + this.setProperties({ + can_approve: false, + approved: true, + approved_by: approvedBy + }); + }); + }, + + setOriginalTrustLevel() { + this.set("originalTrustLevel", this.trust_level); + }, + + dirty: propertyNotEqual("originalTrustLevel", "trust_level"), + + saveTrustLevel() { + return ajax(`/admin/users/${this.id}/trust_level`, { + type: "PUT", + data: { level: this.trust_level } + }) + .then(() => window.location.reload()) + .catch(e => { + let error; + if (e.responseJSON && e.responseJSON.errors) { + error = e.responseJSON.errors[0]; + } + error = + error || + I18n.t("admin.user.trust_level_change_failed", { + error: this._formatError(e) + }); + bootbox.alert(error); + }); + }, + + restoreTrustLevel() { + this.set("trust_level", this.originalTrustLevel); + }, + + lockTrustLevel(locked) { + return ajax(`/admin/users/${this.id}/trust_level_lock`, { + type: "PUT", + data: { locked: !!locked } + }) + .then(() => window.location.reload()) + .catch(e => { + let error; + if (e.responseJSON && e.responseJSON.errors) { + error = e.responseJSON.errors[0]; + } + error = + error || + I18n.t("admin.user.trust_level_change_failed", { + error: this._formatError(e) + }); + bootbox.alert(error); + }); + }, + + canLockTrustLevel: lt("trust_level", 4), + + canSuspend: not("staff"), + + @discourseComputed("suspended_till", "suspended_at") + suspendDuration(suspendedTill, suspendedAt) { + suspendedAt = moment(suspendedAt); + suspendedTill = moment(suspendedTill); + return suspendedAt.format("L") + " - " + suspendedTill.format("L"); + }, + + suspend(data) { + return ajax(`/admin/users/${this.id}/suspend`, { + type: "PUT", + data + }).then(result => this.setProperties(result.suspension)); + }, + + unsuspend() { + return ajax(`/admin/users/${this.id}/unsuspend`, { + type: "PUT" + }).then(result => this.setProperties(result.suspension)); + }, + + logOut() { + return ajax("/admin/users/" + this.id + "/log_out", { + type: "POST", + data: { username_or_email: this.username } + }).then(() => bootbox.alert(I18n.t("admin.user.logged_out"))); + }, + + impersonate() { + return ajax("/admin/impersonate", { + type: "POST", + data: { username_or_email: this.username } + }) + .then(() => (document.location = Discourse.getURL("/"))) + .catch(e => { + if (e.status === 404) { + bootbox.alert(I18n.t("admin.impersonate.not_found")); + } else { + bootbox.alert(I18n.t("admin.impersonate.invalid")); + } + }); + }, + + activate() { + return ajax(`/admin/users/${this.id}/activate`, { + type: "PUT" + }) + .then(() => window.location.reload()) + .catch(e => { + const error = I18n.t("admin.user.activate_failed", { + error: this._formatError(e) + }); + bootbox.alert(error); + }); + }, + + deactivate() { + return ajax(`/admin/users/${this.id}/deactivate`, { + type: "PUT", + data: { context: document.location.pathname } + }) + .then(() => window.location.reload()) + .catch(e => { + const error = I18n.t("admin.user.deactivate_failed", { + error: this._formatError(e) + }); + bootbox.alert(error); + }); + }, + + unsilence() { + this.set("silencingUser", true); + + return ajax(`/admin/users/${this.id}/unsilence`, { + type: "PUT" + }) + .then(result => this.setProperties(result.unsilence)) + .catch(e => { + const error = I18n.t("admin.user.unsilence_failed", { + error: this._formatError(e) + }); + bootbox.alert(error); + }) + .finally(() => this.set("silencingUser", false)); + }, + + silence(data) { + this.set("silencingUser", true); + return ajax(`/admin/users/${this.id}/silence`, { + type: "PUT", + data + }) + .then(result => this.setProperties(result.silence)) + .catch(e => { + const error = I18n.t("admin.user.silence_failed", { + error: this._formatError(e) + }); + bootbox.alert(error); + }) + .finally(() => this.set("silencingUser", false)); + }, + + sendActivationEmail() { + return ajax(userPath("action/send_activation_email"), { + type: "POST", + data: { username: this.username } + }) + .then(() => bootbox.alert(I18n.t("admin.user.activation_email_sent"))) + .catch(popupAjaxError); + }, + + anonymize() { + const user = this; + const message = I18n.t("admin.user.anonymize_confirm"); + + const performAnonymize = function() { + return ajax(`/admin/users/${user.get("id")}/anonymize.json`, { + type: "PUT" + }) + .then(function(data) { + if (data.success) { + if (data.username) { + document.location = Discourse.getURL( + `/admin/users/${user.get("id")}/${data.username}` + ); + } else { + document.location = Discourse.getURL("/admin/users/list/active"); + } + } else { + bootbox.alert(I18n.t("admin.user.anonymize_failed")); + if (data.user) { + user.setProperties(data.user); + } + } + }) + .catch(() => bootbox.alert(I18n.t("admin.user.anonymize_failed"))); + }; + + const buttons = [ + { + label: I18n.t("composer.cancel"), + class: "cancel", + link: true + }, + { + label: + `${iconHTML("exclamation-triangle")} ` + + I18n.t("admin.user.anonymize_yes"), + class: "btn btn-danger", + callback: function() { + performAnonymize(); + } + } + ]; + + bootbox.dialog(message, buttons, { classes: "delete-user-modal" }); + }, + + destroy(opts) { + const user = this; + const message = I18n.t("admin.user.delete_confirm"); + const location = document.location.pathname; + + const performDestroy = function(block) { + bootbox.dialog(I18n.t("admin.user.deleting_user")); + let formData = { context: location }; + if (block) { + formData["block_email"] = true; + formData["block_urls"] = true; + formData["block_ip"] = true; + } + if (opts && opts.deletePosts) { + formData["delete_posts"] = true; + } + return ajax(`/admin/users/${user.get("id")}.json`, { + type: "DELETE", + data: formData + }) + .then(function(data) { + if (data.deleted) { + if (/^\/admin\/users\/list\//.test(location)) { + document.location = location; + } else { + document.location = Discourse.getURL("/admin/users/list/active"); + } + } else { + bootbox.alert(I18n.t("admin.user.delete_failed")); + if (data.user) { + user.setProperties(data.user); + } + } + }) + .catch(function() { + AdminUser.find(user.get("id")).then(u => user.setProperties(u)); + bootbox.alert(I18n.t("admin.user.delete_failed")); + }); + }; + + const buttons = [ + { + label: I18n.t("composer.cancel"), + class: "btn", + link: true + }, + { + label: + `${iconHTML("exclamation-triangle")} ` + + I18n.t("admin.user.delete_and_block"), + class: "btn btn-danger", + callback: function() { + performDestroy(true); + } + }, + { + label: I18n.t("admin.user.delete_dont_block"), + class: "btn btn-primary", + callback: function() { + performDestroy(false); + } + } + ]; + + bootbox.dialog(message, buttons, { classes: "delete-user-modal" }); + }, + + merge(opts) { + const user = this; + const location = document.location.pathname; + + bootbox.dialog(I18n.t("admin.user.merging_user")); + let formData = { context: location }; + + if (opts && opts.targetUsername) { + formData["target_username"] = opts.targetUsername; + } + + return ajax(`/admin/users/${user.get("id")}/merge.json`, { + type: "POST", + data: formData + }) + .then(function(data) { + if (data.merged) { + if (/^\/admin\/users\/list\//.test(location)) { + document.location = location; + } else { + document.location = Discourse.getURL( + `/admin/users/${data.user.id}/${data.user.username}` + ); + } + } else { + bootbox.alert(I18n.t("admin.user.merge_failed")); + if (data.user) { + user.setProperties(data.user); + } + } + }) + .catch(function() { + AdminUser.find(user.get("id")).then(u => user.setProperties(u)); + bootbox.alert(I18n.t("admin.user.merge_failed")); + }); + }, + + loadDetails() { + if (this.loadedDetails) { + return Promise.resolve(this); + } + + return AdminUser.find(this.id).then(result => { + const userProperties = Object.assign(result, { loadedDetails: true }); + this.setProperties(userProperties); + }); + }, + + @discourseComputed("tl3_requirements") + tl3Requirements(requirements) { + if (requirements) { + return this.store.createRecord("tl3Requirements", requirements); + } + }, + + @discourseComputed("suspended_by") + suspendedBy: wrapAdmin, + + @discourseComputed("silenced_by") + silencedBy: wrapAdmin, + + @discourseComputed("approved_by") + approvedBy: wrapAdmin, + + _formatError(event) { + return `http: ${event.status} - ${event.body}`; + } +}); + +AdminUser.reopenClass({ + find(user_id) { + return ajax(`/admin/users/${user_id}.json`).then(result => { + result.loadedDetails = true; + return AdminUser.create(result); + }); + }, + + findAll(query, userFilter) { + return ajax(`/admin/users/list/${query}.json`, { + data: userFilter + }).then(users => users.map(u => AdminUser.create(u))); + } +}); + +export default AdminUser; diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 deleted file mode 100644 index 25a822f388..0000000000 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ /dev/null @@ -1,553 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { filter, or, gt, lt, not } from "@ember/object/computed"; -import { iconHTML } from "discourse-common/lib/icon-library"; -import { ajax } from "discourse/lib/ajax"; -import { propertyNotEqual } from "discourse/lib/computed"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import Group from "discourse/models/group"; -import { userPath } from "discourse/lib/url"; -import { Promise } from "rsvp"; -import User from "discourse/models/user"; - -const wrapAdmin = user => (user ? AdminUser.create(user) : null); - -const AdminUser = User.extend({ - adminUserView: true, - customGroups: filter("groups", g => !g.automatic && Group.create(g)), - automaticGroups: filter("groups", g => g.automatic && Group.create(g)), - - canViewProfile: or("active", "staged"), - - @discourseComputed("bounce_score", "reset_bounce_score_after") - bounceScore(bounce_score, reset_bounce_score_after) { - if (bounce_score > 0) { - return `${bounce_score} - ${moment(reset_bounce_score_after).format( - "LL" - )}`; - } else { - return bounce_score; - } - }, - - @discourseComputed("bounce_score") - bounceScoreExplanation(bounce_score) { - if (bounce_score === 0) { - return I18n.t("admin.user.bounce_score_explanation.none"); - } else if (bounce_score < Discourse.SiteSettings.bounce_score_threshold) { - return I18n.t("admin.user.bounce_score_explanation.some"); - } else { - return I18n.t("admin.user.bounce_score_explanation.threshold_reached"); - } - }, - - @discourseComputed - bounceLink() { - return Discourse.getURL("/admin/email/bounced"); - }, - - canResetBounceScore: gt("bounce_score", 0), - - resetBounceScore() { - return ajax(`/admin/users/${this.id}/reset_bounce_score`, { - type: "POST" - }).then(() => - this.setProperties({ - bounce_score: 0, - reset_bounce_score_after: null - }) - ); - }, - - groupAdded(added) { - return ajax(`/admin/users/${this.id}/groups`, { - type: "POST", - data: { group_id: added.id } - }).then(() => this.groups.pushObject(added)); - }, - - groupRemoved(groupId) { - return ajax(`/admin/users/${this.id}/groups/${groupId}`, { - type: "DELETE" - }).then(() => { - this.set("groups.[]", this.groups.rejectBy("id", groupId)); - if (this.primary_group_id === groupId) { - this.set("primary_group_id", null); - } - }); - }, - - revokeApiKey() { - return ajax(`/admin/users/${this.id}/revoke_api_key`, { - type: "DELETE" - }).then(() => this.set("api_key", null)); - }, - - deleteAllPosts() { - let deletedPosts = 0; - const user = this; - const message = I18n.messageFormat( - "admin.user.delete_all_posts_confirm_MF", - { - POSTS: user.get("post_count"), - TOPICS: user.get("topic_count") - } - ); - const buttons = [ - { - label: I18n.t("composer.cancel"), - class: "d-modal-cancel", - link: true - }, - { - label: - `${iconHTML("exclamation-triangle")} ` + - I18n.t("admin.user.delete_all_posts"), - class: "btn btn-danger", - callback: () => { - openProgressModal(); - performDelete(); - } - } - ]; - const openProgressModal = () => { - bootbox.dialog( - `

${I18n.t( - "admin.user.delete_posts_progress" - )}

`, - [], - { classes: "delete-posts-progress" } - ); - }; - const performDelete = () => { - let deletedPercentage = 0; - return ajax(`/admin/users/${user.get("id")}/delete_posts_batch`, { - type: "PUT" - }) - .then(({ posts_deleted }) => { - if (posts_deleted === 0) { - user.set("post_count", 0); - bootbox.hideAll(); - } else { - deletedPosts += posts_deleted; - deletedPercentage = Math.floor( - (deletedPosts * 100) / user.get("post_count") - ); - $(".delete-posts-progress .progress-bar > span").css({ - width: `${deletedPercentage}%` - }); - performDelete(); - } - }) - .catch(e => { - bootbox.hideAll(); - let error; - AdminUser.find(user.get("id")).then(u => user.setProperties(u)); - if (e.responseJSON && e.responseJSON.errors) { - error = e.responseJSON.errors[0]; - } - error = error || I18n.t("admin.user.delete_posts_failed"); - bootbox.alert(error); - }); - }; - - bootbox.dialog(message, buttons, { classes: "delete-all-posts" }); - }, - - revokeAdmin() { - return ajax(`/admin/users/${this.id}/revoke_admin`, { - type: "PUT" - }).then(() => { - this.setProperties({ - admin: false, - can_grant_admin: true, - can_revoke_admin: false - }); - }); - }, - - grantAdmin() { - return ajax(`/admin/users/${this.id}/grant_admin`, { - type: "PUT" - }) - .then(() => { - bootbox.alert(I18n.t("admin.user.grant_admin_confirm")); - }) - .catch(popupAjaxError); - }, - - revokeModeration() { - return ajax(`/admin/users/${this.id}/revoke_moderation`, { - type: "PUT" - }) - .then(() => { - this.setProperties({ - moderator: false, - can_grant_moderation: true, - can_revoke_moderation: false - }); - }) - .catch(popupAjaxError); - }, - - grantModeration() { - return ajax(`/admin/users/${this.id}/grant_moderation`, { - type: "PUT" - }) - .then(() => { - this.setProperties({ - moderator: true, - can_grant_moderation: false, - can_revoke_moderation: true - }); - }) - .catch(popupAjaxError); - }, - - disableSecondFactor() { - return ajax(`/admin/users/${this.id}/disable_second_factor`, { - type: "PUT" - }) - .then(() => { - this.set("second_factor_enabled", false); - }) - .catch(popupAjaxError); - }, - - approve(approvedBy) { - return ajax(`/admin/users/${this.id}/approve`, { - type: "PUT" - }).then(() => { - this.setProperties({ - can_approve: false, - approved: true, - approved_by: approvedBy - }); - }); - }, - - setOriginalTrustLevel() { - this.set("originalTrustLevel", this.trust_level); - }, - - dirty: propertyNotEqual("originalTrustLevel", "trust_level"), - - saveTrustLevel() { - return ajax(`/admin/users/${this.id}/trust_level`, { - type: "PUT", - data: { level: this.trust_level } - }) - .then(() => window.location.reload()) - .catch(e => { - let error; - if (e.responseJSON && e.responseJSON.errors) { - error = e.responseJSON.errors[0]; - } - error = - error || - I18n.t("admin.user.trust_level_change_failed", { - error: this._formatError(e) - }); - bootbox.alert(error); - }); - }, - - restoreTrustLevel() { - this.set("trust_level", this.originalTrustLevel); - }, - - lockTrustLevel(locked) { - return ajax(`/admin/users/${this.id}/trust_level_lock`, { - type: "PUT", - data: { locked: !!locked } - }) - .then(() => window.location.reload()) - .catch(e => { - let error; - if (e.responseJSON && e.responseJSON.errors) { - error = e.responseJSON.errors[0]; - } - error = - error || - I18n.t("admin.user.trust_level_change_failed", { - error: this._formatError(e) - }); - bootbox.alert(error); - }); - }, - - canLockTrustLevel: lt("trust_level", 4), - - canSuspend: not("staff"), - - @discourseComputed("suspended_till", "suspended_at") - suspendDuration(suspendedTill, suspendedAt) { - suspendedAt = moment(suspendedAt); - suspendedTill = moment(suspendedTill); - return suspendedAt.format("L") + " - " + suspendedTill.format("L"); - }, - - suspend(data) { - return ajax(`/admin/users/${this.id}/suspend`, { - type: "PUT", - data - }).then(result => this.setProperties(result.suspension)); - }, - - unsuspend() { - return ajax(`/admin/users/${this.id}/unsuspend`, { - type: "PUT" - }).then(result => this.setProperties(result.suspension)); - }, - - logOut() { - return ajax("/admin/users/" + this.id + "/log_out", { - type: "POST", - data: { username_or_email: this.username } - }).then(() => bootbox.alert(I18n.t("admin.user.logged_out"))); - }, - - impersonate() { - return ajax("/admin/impersonate", { - type: "POST", - data: { username_or_email: this.username } - }) - .then(() => (document.location = Discourse.getURL("/"))) - .catch(e => { - if (e.status === 404) { - bootbox.alert(I18n.t("admin.impersonate.not_found")); - } else { - bootbox.alert(I18n.t("admin.impersonate.invalid")); - } - }); - }, - - activate() { - return ajax(`/admin/users/${this.id}/activate`, { - type: "PUT" - }) - .then(() => window.location.reload()) - .catch(e => { - const error = I18n.t("admin.user.activate_failed", { - error: this._formatError(e) - }); - bootbox.alert(error); - }); - }, - - deactivate() { - return ajax(`/admin/users/${this.id}/deactivate`, { - type: "PUT", - data: { context: document.location.pathname } - }) - .then(() => window.location.reload()) - .catch(e => { - const error = I18n.t("admin.user.deactivate_failed", { - error: this._formatError(e) - }); - bootbox.alert(error); - }); - }, - - unsilence() { - this.set("silencingUser", true); - - return ajax(`/admin/users/${this.id}/unsilence`, { - type: "PUT" - }) - .then(result => this.setProperties(result.unsilence)) - .catch(e => { - const error = I18n.t("admin.user.unsilence_failed", { - error: this._formatError(e) - }); - bootbox.alert(error); - }) - .finally(() => this.set("silencingUser", false)); - }, - - silence(data) { - this.set("silencingUser", true); - return ajax(`/admin/users/${this.id}/silence`, { - type: "PUT", - data - }) - .then(result => this.setProperties(result.silence)) - .catch(e => { - const error = I18n.t("admin.user.silence_failed", { - error: this._formatError(e) - }); - bootbox.alert(error); - }) - .finally(() => this.set("silencingUser", false)); - }, - - sendActivationEmail() { - return ajax(userPath("action/send_activation_email"), { - type: "POST", - data: { username: this.username } - }) - .then(() => bootbox.alert(I18n.t("admin.user.activation_email_sent"))) - .catch(popupAjaxError); - }, - - anonymize() { - const user = this; - const message = I18n.t("admin.user.anonymize_confirm"); - - const performAnonymize = function() { - return ajax(`/admin/users/${user.get("id")}/anonymize.json`, { - type: "PUT" - }) - .then(function(data) { - if (data.success) { - if (data.username) { - document.location = Discourse.getURL( - `/admin/users/${user.get("id")}/${data.username}` - ); - } else { - document.location = Discourse.getURL("/admin/users/list/active"); - } - } else { - bootbox.alert(I18n.t("admin.user.anonymize_failed")); - if (data.user) { - user.setProperties(data.user); - } - } - }) - .catch(() => bootbox.alert(I18n.t("admin.user.anonymize_failed"))); - }; - - const buttons = [ - { - label: I18n.t("composer.cancel"), - class: "cancel", - link: true - }, - { - label: - `${iconHTML("exclamation-triangle")} ` + - I18n.t("admin.user.anonymize_yes"), - class: "btn btn-danger", - callback: function() { - performAnonymize(); - } - } - ]; - - bootbox.dialog(message, buttons, { classes: "delete-user-modal" }); - }, - - destroy(opts) { - const user = this; - const message = I18n.t("admin.user.delete_confirm"); - const location = document.location.pathname; - - const performDestroy = function(block) { - bootbox.dialog(I18n.t("admin.user.deleting_user")); - let formData = { context: location }; - if (block) { - formData["block_email"] = true; - formData["block_urls"] = true; - formData["block_ip"] = true; - } - if (opts && opts.deletePosts) { - formData["delete_posts"] = true; - } - return ajax(`/admin/users/${user.get("id")}.json`, { - type: "DELETE", - data: formData - }) - .then(function(data) { - if (data.deleted) { - if (/^\/admin\/users\/list\//.test(location)) { - document.location = location; - } else { - document.location = Discourse.getURL("/admin/users/list/active"); - } - } else { - bootbox.alert(I18n.t("admin.user.delete_failed")); - if (data.user) { - user.setProperties(data.user); - } - } - }) - .catch(function() { - AdminUser.find(user.get("id")).then(u => user.setProperties(u)); - bootbox.alert(I18n.t("admin.user.delete_failed")); - }); - }; - - const buttons = [ - { - label: I18n.t("composer.cancel"), - class: "btn", - link: true - }, - { - label: - `${iconHTML("exclamation-triangle")} ` + - I18n.t("admin.user.delete_and_block"), - class: "btn btn-danger", - callback: function() { - performDestroy(true); - } - }, - { - label: I18n.t("admin.user.delete_dont_block"), - class: "btn btn-primary", - callback: function() { - performDestroy(false); - } - } - ]; - - bootbox.dialog(message, buttons, { classes: "delete-user-modal" }); - }, - - loadDetails() { - if (this.loadedDetails) { - return Promise.resolve(this); - } - - return AdminUser.find(this.id).then(result => { - const userProperties = Object.assign(result, { loadedDetails: true }); - this.setProperties(userProperties); - }); - }, - - @discourseComputed("tl3_requirements") - tl3Requirements(requirements) { - if (requirements) { - return this.store.createRecord("tl3Requirements", requirements); - } - }, - - @discourseComputed("suspended_by") - suspendedBy: wrapAdmin, - - @discourseComputed("silenced_by") - silencedBy: wrapAdmin, - - @discourseComputed("approved_by") - approvedBy: wrapAdmin, - - _formatError(event) { - return `http: ${event.status} - ${event.body}`; - } -}); - -AdminUser.reopenClass({ - find(user_id) { - return ajax(`/admin/users/${user_id}.json`).then(result => { - result.loadedDetails = true; - return AdminUser.create(result); - }); - }, - - findAll(query, userFilter) { - return ajax(`/admin/users/list/${query}.json`, { - data: userFilter - }).then(users => users.map(u => AdminUser.create(u))); - } -}); - -export default AdminUser; diff --git a/app/assets/javascripts/admin/models/api-key.js.es6 b/app/assets/javascripts/admin/models/api-key.js similarity index 100% rename from app/assets/javascripts/admin/models/api-key.js.es6 rename to app/assets/javascripts/admin/models/api-key.js diff --git a/app/assets/javascripts/admin/models/backup-status.js.es6 b/app/assets/javascripts/admin/models/backup-status.js similarity index 100% rename from app/assets/javascripts/admin/models/backup-status.js.es6 rename to app/assets/javascripts/admin/models/backup-status.js diff --git a/app/assets/javascripts/admin/models/backup.js.es6 b/app/assets/javascripts/admin/models/backup.js similarity index 100% rename from app/assets/javascripts/admin/models/backup.js.es6 rename to app/assets/javascripts/admin/models/backup.js diff --git a/app/assets/javascripts/admin/models/color-scheme-color.js.es6 b/app/assets/javascripts/admin/models/color-scheme-color.js similarity index 100% rename from app/assets/javascripts/admin/models/color-scheme-color.js.es6 rename to app/assets/javascripts/admin/models/color-scheme-color.js diff --git a/app/assets/javascripts/admin/models/color-scheme.js b/app/assets/javascripts/admin/models/color-scheme.js new file mode 100644 index 0000000000..ee60097b74 --- /dev/null +++ b/app/assets/javascripts/admin/models/color-scheme.js @@ -0,0 +1,147 @@ +import { A } from "@ember/array"; +import ArrayProxy from "@ember/array/proxy"; +import discourseComputed from "discourse-common/utils/decorators"; +import { not } from "@ember/object/computed"; +import { ajax } from "discourse/lib/ajax"; +import ColorSchemeColor from "admin/models/color-scheme-color"; +import EmberObject from "@ember/object"; + +const ColorScheme = EmberObject.extend({ + init() { + this._super(...arguments); + + this.startTrackingChanges(); + }, + + @discourseComputed + description() { + return "" + this.name; + }, + + startTrackingChanges() { + this.set("originals", { name: this.name }); + }, + + schemeJson() { + const buffer = []; + this.colors.forEach(c => { + buffer.push(` "${c.get("name")}": "${c.get("hex")}"`); + }); + + return [`"${this.name}": {`, buffer.join(",\n"), "}"].join("\n"); + }, + + copy() { + const newScheme = ColorScheme.create({ + name: this.name, + can_edit: true, + colors: A() + }); + this.colors.forEach(c => { + newScheme.colors.pushObject( + ColorSchemeColor.create(c.getProperties("name", "hex", "default_hex")) + ); + }); + return newScheme; + }, + + @discourseComputed("name", "colors.@each.changed", "saving") + changed(name) { + if (!this.originals) return false; + if (this.originals.name !== name) return true; + if (this.colors.any(c => c.get("changed"))) return true; + + return false; + }, + + @discourseComputed("changed") + disableSave(changed) { + if (this.theme_id) { + return false; + } + + return !changed || this.saving || this.colors.any(c => !c.get("valid")); + }, + + newRecord: not("id"), + + save(opts) { + if (this.is_base || this.disableSave) return; + + this.setProperties({ savingStatus: I18n.t("saving"), saving: true }); + + const data = {}; + + if (!opts || !opts.enabledOnly) { + data.name = this.name; + data.base_scheme_id = this.base_scheme_id; + data.colors = []; + this.colors.forEach(c => { + if (!this.id || c.get("changed")) { + data.colors.pushObject(c.getProperties("name", "hex")); + } + }); + } + + return ajax( + "/admin/color_schemes" + (this.id ? "/" + this.id : "") + ".json", + { + data: JSON.stringify({ color_scheme: data }), + type: this.id ? "PUT" : "POST", + dataType: "json", + contentType: "application/json" + } + ).then(result => { + if (result.id) { + this.set("id", result.id); + } + + if (!opts || !opts.enabledOnly) { + this.startTrackingChanges(); + this.colors.forEach(c => c.startTrackingChanges()); + } + + this.setProperties({ savingStatus: I18n.t("saved"), saving: false }); + this.notifyPropertyChange("description"); + }); + }, + + destroy() { + if (this.id) { + return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" }); + } + } +}); + +const ColorSchemes = ArrayProxy.extend({}); + +ColorScheme.reopenClass({ + findAll() { + const colorSchemes = ColorSchemes.create({ content: [], loading: true }); + return ajax("/admin/color_schemes").then(all => { + all.forEach(colorScheme => { + colorSchemes.pushObject( + ColorScheme.create({ + id: colorScheme.id, + name: colorScheme.name, + is_base: colorScheme.is_base, + theme_id: colorScheme.theme_id, + theme_name: colorScheme.theme_name, + base_scheme_id: colorScheme.base_scheme_id, + colors: colorScheme.colors.map(c => { + return ColorSchemeColor.create({ + name: c.name, + hex: c.hex, + default_hex: c.default_hex, + is_advanced: c.is_advanced + }); + }) + }) + ); + }); + return colorSchemes; + }); + } +}); + +export default ColorScheme; diff --git a/app/assets/javascripts/admin/models/color-scheme.js.es6 b/app/assets/javascripts/admin/models/color-scheme.js.es6 deleted file mode 100644 index afabb1ed6e..0000000000 --- a/app/assets/javascripts/admin/models/color-scheme.js.es6 +++ /dev/null @@ -1,145 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { not } from "@ember/object/computed"; -import { ajax } from "discourse/lib/ajax"; -import ColorSchemeColor from "admin/models/color-scheme-color"; -import EmberObject from "@ember/object"; - -const ColorScheme = EmberObject.extend({ - init() { - this._super(...arguments); - - this.startTrackingChanges(); - }, - - @discourseComputed - description() { - return "" + this.name; - }, - - startTrackingChanges() { - this.set("originals", { name: this.name }); - }, - - schemeJson() { - const buffer = []; - this.colors.forEach(c => { - buffer.push(` "${c.get("name")}": "${c.get("hex")}"`); - }); - - return [`"${this.name}": {`, buffer.join(",\n"), "}"].join("\n"); - }, - - copy() { - const newScheme = ColorScheme.create({ - name: this.name, - can_edit: true, - colors: Ember.A() - }); - this.colors.forEach(c => { - newScheme.colors.pushObject( - ColorSchemeColor.create(c.getProperties("name", "hex", "default_hex")) - ); - }); - return newScheme; - }, - - @discourseComputed("name", "colors.@each.changed", "saving") - changed(name) { - if (!this.originals) return false; - if (this.originals.name !== name) return true; - if (this.colors.any(c => c.get("changed"))) return true; - - return false; - }, - - @discourseComputed("changed") - disableSave(changed) { - if (this.theme_id) { - return false; - } - - return !changed || this.saving || this.colors.any(c => !c.get("valid")); - }, - - newRecord: not("id"), - - save(opts) { - if (this.is_base || this.disableSave) return; - - this.setProperties({ savingStatus: I18n.t("saving"), saving: true }); - - const data = {}; - - if (!opts || !opts.enabledOnly) { - data.name = this.name; - data.base_scheme_id = this.base_scheme_id; - data.colors = []; - this.colors.forEach(c => { - if (!this.id || c.get("changed")) { - data.colors.pushObject(c.getProperties("name", "hex")); - } - }); - } - - return ajax( - "/admin/color_schemes" + (this.id ? "/" + this.id : "") + ".json", - { - data: JSON.stringify({ color_scheme: data }), - type: this.id ? "PUT" : "POST", - dataType: "json", - contentType: "application/json" - } - ).then(result => { - if (result.id) { - this.set("id", result.id); - } - - if (!opts || !opts.enabledOnly) { - this.startTrackingChanges(); - this.colors.forEach(c => c.startTrackingChanges()); - } - - this.setProperties({ savingStatus: I18n.t("saved"), saving: false }); - this.notifyPropertyChange("description"); - }); - }, - - destroy() { - if (this.id) { - return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" }); - } - } -}); - -const ColorSchemes = Ember.ArrayProxy.extend({}); - -ColorScheme.reopenClass({ - findAll() { - const colorSchemes = ColorSchemes.create({ content: [], loading: true }); - return ajax("/admin/color_schemes").then(all => { - all.forEach(colorScheme => { - colorSchemes.pushObject( - ColorScheme.create({ - id: colorScheme.id, - name: colorScheme.name, - is_base: colorScheme.is_base, - theme_id: colorScheme.theme_id, - theme_name: colorScheme.theme_name, - base_scheme_id: colorScheme.base_scheme_id, - colors: colorScheme.colors.map(c => { - return ColorSchemeColor.create({ - name: c.name, - hex: c.hex, - default_hex: c.default_hex, - is_advanced: c.is_advanced - }); - }) - }) - ); - }); - return colorSchemes; - }); - } -}); - -export default ColorScheme; diff --git a/app/assets/javascripts/admin/models/email-log.js.es6 b/app/assets/javascripts/admin/models/email-log.js similarity index 100% rename from app/assets/javascripts/admin/models/email-log.js.es6 rename to app/assets/javascripts/admin/models/email-log.js diff --git a/app/assets/javascripts/admin/models/email-preview.js.es6 b/app/assets/javascripts/admin/models/email-preview.js similarity index 100% rename from app/assets/javascripts/admin/models/email-preview.js.es6 rename to app/assets/javascripts/admin/models/email-preview.js diff --git a/app/assets/javascripts/admin/models/email-settings.js.es6 b/app/assets/javascripts/admin/models/email-settings.js similarity index 100% rename from app/assets/javascripts/admin/models/email-settings.js.es6 rename to app/assets/javascripts/admin/models/email-settings.js diff --git a/app/assets/javascripts/admin/models/email-style.js.es6 b/app/assets/javascripts/admin/models/email-style.js similarity index 100% rename from app/assets/javascripts/admin/models/email-style.js.es6 rename to app/assets/javascripts/admin/models/email-style.js diff --git a/app/assets/javascripts/admin/models/email-template.js b/app/assets/javascripts/admin/models/email-template.js new file mode 100644 index 0000000000..550148e0ab --- /dev/null +++ b/app/assets/javascripts/admin/models/email-template.js @@ -0,0 +1,13 @@ +import { ajax } from "discourse/lib/ajax"; +import RestModel from "discourse/models/rest"; +const { getProperties } = Ember; + +export default RestModel.extend({ + revert() { + return ajax(`/admin/customize/email_templates/${this.id}`, { + type: "DELETE" + }).then(result => + getProperties(result.email_template, "subject", "body", "can_revert") + ); + } +}); diff --git a/app/assets/javascripts/admin/models/email-template.js.es6 b/app/assets/javascripts/admin/models/email-template.js.es6 deleted file mode 100644 index 2dddb272dc..0000000000 --- a/app/assets/javascripts/admin/models/email-template.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import RestModel from "discourse/models/rest"; -const { getProperties } = Ember; - -export default RestModel.extend({ - revert() { - return ajax(`/admin/customize/email_templates/${this.id}`, { - method: "DELETE" - }).then(result => - getProperties(result.email_template, "subject", "body", "can_revert") - ); - } -}); diff --git a/app/assets/javascripts/admin/models/flag-type.js.es6 b/app/assets/javascripts/admin/models/flag-type.js similarity index 100% rename from app/assets/javascripts/admin/models/flag-type.js.es6 rename to app/assets/javascripts/admin/models/flag-type.js diff --git a/app/assets/javascripts/admin/models/incoming-email.js.es6 b/app/assets/javascripts/admin/models/incoming-email.js similarity index 100% rename from app/assets/javascripts/admin/models/incoming-email.js.es6 rename to app/assets/javascripts/admin/models/incoming-email.js diff --git a/app/assets/javascripts/admin/models/permalink.js b/app/assets/javascripts/admin/models/permalink.js new file mode 100644 index 0000000000..b13dcd6f52 --- /dev/null +++ b/app/assets/javascripts/admin/models/permalink.js @@ -0,0 +1,46 @@ +import { ajax } from "discourse/lib/ajax"; +import discourseComputed from "discourse-common/utils/decorators"; +import DiscourseURL from "discourse/lib/url"; +import Category from "discourse/models/category"; +import EmberObject from "@ember/object"; + +const Permalink = EmberObject.extend({ + save: function() { + return ajax("/admin/permalinks.json", { + type: "POST", + data: { + url: this.url, + permalink_type: this.permalink_type, + permalink_type_value: this.permalink_type_value + } + }); + }, + + @discourseComputed("category_id") + category: function(category_id) { + return Category.findById(category_id); + }, + + @discourseComputed("external_url") + linkIsExternal: function(external_url) { + return !DiscourseURL.isInternal(external_url); + }, + + destroy: function() { + return ajax("/admin/permalinks/" + this.id + ".json", { + type: "DELETE" + }); + } +}); + +Permalink.reopenClass({ + findAll: function(filter) { + return ajax("/admin/permalinks.json", { data: { filter: filter } }).then( + function(permalinks) { + return permalinks.map(p => Permalink.create(p)); + } + ); + } +}); + +export default Permalink; diff --git a/app/assets/javascripts/admin/models/permalink.js.es6 b/app/assets/javascripts/admin/models/permalink.js.es6 deleted file mode 100644 index b86e931692..0000000000 --- a/app/assets/javascripts/admin/models/permalink.js.es6 +++ /dev/null @@ -1,33 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const Permalink = EmberObject.extend({ - save: function() { - return ajax("/admin/permalinks.json", { - type: "POST", - data: { - url: this.url, - permalink_type: this.permalink_type, - permalink_type_value: this.permalink_type_value - } - }); - }, - - destroy: function() { - return ajax("/admin/permalinks/" + this.id + ".json", { - type: "DELETE" - }); - } -}); - -Permalink.reopenClass({ - findAll: function(filter) { - return ajax("/admin/permalinks.json", { data: { filter: filter } }).then( - function(permalinks) { - return permalinks.map(p => Permalink.create(p)); - } - ); - } -}); - -export default Permalink; diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js similarity index 100% rename from app/assets/javascripts/admin/models/report.js.es6 rename to app/assets/javascripts/admin/models/report.js diff --git a/app/assets/javascripts/admin/models/screened-email.js b/app/assets/javascripts/admin/models/screened-email.js new file mode 100644 index 0000000000..4ef3217edf --- /dev/null +++ b/app/assets/javascripts/admin/models/screened-email.js @@ -0,0 +1,30 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { ajax } from "discourse/lib/ajax"; +import EmberObject from "@ember/object"; + +const ScreenedEmail = EmberObject.extend({ + @discourseComputed("action") + actionName(action) { + return I18n.t("admin.logs.screened_actions." + action); + }, + + clearBlock: function() { + return ajax("/admin/logs/screened_emails/" + this.id, { + type: "DELETE" + }); + } +}); + +ScreenedEmail.reopenClass({ + findAll: function() { + return ajax("/admin/logs/screened_emails.json").then(function( + screened_emails + ) { + return screened_emails.map(function(b) { + return ScreenedEmail.create(b); + }); + }); + } +}); + +export default ScreenedEmail; diff --git a/app/assets/javascripts/admin/models/screened-email.js.es6 b/app/assets/javascripts/admin/models/screened-email.js.es6 deleted file mode 100644 index ea72510551..0000000000 --- a/app/assets/javascripts/admin/models/screened-email.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const ScreenedEmail = EmberObject.extend({ - @discourseComputed("action") - actionName(action) { - return I18n.t("admin.logs.screened_actions." + action); - }, - - clearBlock: function() { - return ajax("/admin/logs/screened_emails/" + this.id, { - method: "DELETE" - }); - } -}); - -ScreenedEmail.reopenClass({ - findAll: function() { - return ajax("/admin/logs/screened_emails.json").then(function( - screened_emails - ) { - return screened_emails.map(function(b) { - return ScreenedEmail.create(b); - }); - }); - } -}); - -export default ScreenedEmail; diff --git a/app/assets/javascripts/admin/models/screened-ip-address.js.es6 b/app/assets/javascripts/admin/models/screened-ip-address.js similarity index 100% rename from app/assets/javascripts/admin/models/screened-ip-address.js.es6 rename to app/assets/javascripts/admin/models/screened-ip-address.js diff --git a/app/assets/javascripts/admin/models/screened-url.js.es6 b/app/assets/javascripts/admin/models/screened-url.js similarity index 100% rename from app/assets/javascripts/admin/models/screened-url.js.es6 rename to app/assets/javascripts/admin/models/screened-url.js diff --git a/app/assets/javascripts/admin/models/site-setting.js.es6 b/app/assets/javascripts/admin/models/site-setting.js similarity index 100% rename from app/assets/javascripts/admin/models/site-setting.js.es6 rename to app/assets/javascripts/admin/models/site-setting.js diff --git a/app/assets/javascripts/admin/models/site-text.js b/app/assets/javascripts/admin/models/site-text.js new file mode 100644 index 0000000000..793ab33b54 --- /dev/null +++ b/app/assets/javascripts/admin/models/site-text.js @@ -0,0 +1,11 @@ +import { ajax } from "discourse/lib/ajax"; +import RestModel from "discourse/models/rest"; +const { getProperties } = Ember; + +export default RestModel.extend({ + revert() { + return ajax(`/admin/customize/site_texts/${this.id}`, { + type: "DELETE" + }).then(result => getProperties(result.site_text, "value", "can_revert")); + } +}); diff --git a/app/assets/javascripts/admin/models/site-text.js.es6 b/app/assets/javascripts/admin/models/site-text.js.es6 deleted file mode 100644 index 8bcb8c7f0f..0000000000 --- a/app/assets/javascripts/admin/models/site-text.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import RestModel from "discourse/models/rest"; -const { getProperties } = Ember; - -export default RestModel.extend({ - revert() { - return ajax(`/admin/customize/site_texts/${this.id}`, { - method: "DELETE" - }).then(result => getProperties(result.site_text, "value", "can_revert")); - } -}); diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js similarity index 100% rename from app/assets/javascripts/admin/models/staff-action-log.js.es6 rename to app/assets/javascripts/admin/models/staff-action-log.js diff --git a/app/assets/javascripts/admin/models/theme-settings.js.es6 b/app/assets/javascripts/admin/models/theme-settings.js similarity index 100% rename from app/assets/javascripts/admin/models/theme-settings.js.es6 rename to app/assets/javascripts/admin/models/theme-settings.js diff --git a/app/assets/javascripts/admin/models/theme.js b/app/assets/javascripts/admin/models/theme.js new file mode 100644 index 0000000000..e63e9e1cce --- /dev/null +++ b/app/assets/javascripts/admin/models/theme.js @@ -0,0 +1,352 @@ +import { get } from "@ember/object"; +import { isBlank, isEmpty } from "@ember/utils"; +import { or, gt } from "@ember/object/computed"; +import RestModel from "discourse/models/rest"; +import discourseComputed from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ajax } from "discourse/lib/ajax"; +import { escapeExpression } from "discourse/lib/utilities"; +import highlightSyntax from "discourse/lib/highlight-syntax"; +import { url } from "discourse/lib/computed"; + +const THEME_UPLOAD_VAR = 2; +const FIELDS_IDS = [0, 1, 5]; + +export const THEMES = "themes"; +export const COMPONENTS = "components"; +const SETTINGS_TYPE_ID = 5; + +const Theme = RestModel.extend({ + isActive: or("default", "user_selectable"), + isPendingUpdates: gt("remote_theme.commits_behind", 0), + hasEditedFields: gt("editedFields.length", 0), + hasParents: gt("parent_themes.length", 0), + diffLocalChangesUrl: url("id", "/admin/themes/%@/diff_local_changes"), + + @discourseComputed("theme_fields.[]") + targets() { + return [ + { id: 0, name: "common" }, + { id: 1, name: "desktop", icon: "desktop" }, + { id: 2, name: "mobile", icon: "mobile-alt" }, + { id: 3, name: "settings", icon: "cog", advanced: true }, + { + id: 4, + name: "translations", + icon: "globe", + advanced: true, + customNames: true + }, + { + id: 5, + name: "extra_scss", + icon: "paint-brush", + advanced: true, + customNames: true + } + ].map(target => { + target["edited"] = this.hasEdited(target.name); + target["error"] = this.hasError(target.name); + return target; + }); + }, + + @discourseComputed("theme_fields.[]") + fieldNames() { + const common = [ + "scss", + "head_tag", + "header", + "after_header", + "body_tag", + "footer" + ]; + + const scss_fields = (this.theme_fields || []) + .filter(f => f.target === "extra_scss" && f.name !== "") + .map(f => f.name); + + if (scss_fields.length < 1) { + scss_fields.push("importable_scss"); + } + + return { + common: [...common, "embedded_scss"], + desktop: common, + mobile: common, + settings: ["yaml"], + translations: [ + "en", + ...(this.theme_fields || []) + .filter(f => f.target === "translations" && f.name !== "en") + .map(f => f.name) + ], + extra_scss: scss_fields + }; + }, + + @discourseComputed( + "fieldNames", + "theme_fields.[]", + "theme_fields.@each.error" + ) + fields(fieldNames) { + const hash = {}; + Object.keys(fieldNames).forEach(target => { + hash[target] = fieldNames[target].map(fieldName => { + const field = { + name: fieldName, + edited: this.hasEdited(target, fieldName), + error: this.hasError(target, fieldName) + }; + + if (target === "translations" || target === "extra_scss") { + field.translatedName = fieldName; + } else { + field.translatedName = I18n.t( + `admin.customize.theme.${fieldName}.text` + ); + field.title = I18n.t(`admin.customize.theme.${fieldName}.title`); + } + + if (fieldName.indexOf("_tag") > 0) { + field.icon = "far-file-alt"; + } + + return field; + }); + }); + return hash; + }, + + @discourseComputed("theme_fields") + themeFields(fields) { + if (!fields) { + this.set("theme_fields", []); + return {}; + } + + let hash = {}; + fields.forEach(field => { + if (!field.type_id || FIELDS_IDS.includes(field.type_id)) { + hash[this.getKey(field)] = field; + } + }); + return hash; + }, + + @discourseComputed("theme_fields", "theme_fields.[]") + uploads(fields) { + if (!fields) { + return []; + } + return fields.filter( + f => f.target === "common" && f.type_id === THEME_UPLOAD_VAR + ); + }, + + @discourseComputed("theme_fields", "theme_fields.@each.error") + isBroken(fields) { + return fields && fields.any(field => field.error && field.error.length > 0); + }, + + @discourseComputed("theme_fields.[]") + editedFields(fields) { + return fields.filter( + field => !isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID + ); + }, + + @discourseComputed("remote_theme.last_error_text") + remoteError(errorText) { + if (errorText && errorText.length > 0) { + return errorText; + } + }, + + getKey(field) { + return `${field.target} ${field.name}`; + }, + + hasEdited(target, name) { + if (name) { + return !isEmpty(this.getField(target, name)); + } else { + let fields = this.theme_fields || []; + return fields.any( + field => field.target === target && !isEmpty(field.value) + ); + } + }, + + hasError(target, name) { + return this.theme_fields + .filter(f => f.target === target && (!name || name === f.name)) + .any(f => f.error); + }, + + getError(target, name) { + let themeFields = this.themeFields; + let key = this.getKey({ target, name }); + let field = themeFields[key]; + return field ? field.error : ""; + }, + + getField(target, name) { + let themeFields = this.themeFields; + let key = this.getKey({ target, name }); + let field = themeFields[key]; + return field ? field.value : ""; + }, + + removeField(field) { + this.set("changed", true); + + field.upload_id = null; + field.value = null; + + return this.saveChanges("theme_fields"); + }, + + setField(target, name, value, upload_id, type_id) { + this.set("changed", true); + let themeFields = this.themeFields; + let field = { name, target, value, upload_id, type_id }; + + // slow path for uploads and so on + if (type_id && type_id > 1) { + let fields = this.theme_fields; + let existing = fields.find( + f => f.target === target && f.name === name && f.type_id === type_id + ); + if (existing) { + existing.value = value; + existing.upload_id = upload_id; + } else { + fields.pushObject(field); + } + return; + } + + // fast path + let key = this.getKey({ target, name }); + let existingField = themeFields[key]; + if (!existingField) { + this.theme_fields.pushObject(field); + themeFields[key] = field; + } else { + const changed = + (isEmpty(existingField.value) && !isEmpty(value)) || + (isEmpty(value) && !isEmpty(existingField.value)); + + existingField.value = value; + if (changed) { + // Observing theme_fields.@each.value is too slow, so manually notify + // if the value goes to/from blank + this.notifyPropertyChange("theme_fields.[]"); + } + } + }, + + @discourseComputed("childThemes.[]") + child_theme_ids(childThemes) { + if (childThemes) { + return childThemes.map(theme => get(theme, "id")); + } + }, + + removeChildTheme(theme) { + const childThemes = this.childThemes; + childThemes.removeObject(theme); + return this.saveChanges("child_theme_ids"); + }, + + addChildTheme(theme) { + let childThemes = this.childThemes; + if (!childThemes) { + childThemes = []; + this.set("childThemes", childThemes); + } + childThemes.removeObject(theme); + childThemes.pushObject(theme); + return this.saveChanges("child_theme_ids"); + }, + + addParentTheme(theme) { + let parentThemes = this.parentThemes; + if (!parentThemes) { + parentThemes = []; + this.set("parentThemes", parentThemes); + } + parentThemes.addObject(theme); + }, + + @discourseComputed("name", "default") + description: function(name, isDefault) { + if (isDefault) { + return I18n.t("admin.customize.theme.default_name", { name: name }); + } else { + return name; + } + }, + + checkForUpdates() { + return this.save({ remote_check: true }).then(() => + this.set("changed", false) + ); + }, + + updateToLatest() { + return ajax(this.diffLocalChangesUrl).then(json => { + if (json && json.error) { + bootbox.alert( + I18n.t("generic_error_with_reason", { + error: json.error + }) + ); + } else if (json && json.diff) { + bootbox.confirm( + I18n.t("admin.customize.theme.update_confirm") + + `
${escapeExpression(
+              json.diff
+            )}
`, + I18n.t("cancel"), + I18n.t("admin.customize.theme.update_confirm_yes"), + result => { + if (result) { + return this.save({ remote_update: true }).then(() => + this.set("changed", false) + ); + } + } + ); + highlightSyntax(); + } else { + return this.save({ remote_update: true }).then(() => + this.set("changed", false) + ); + } + }); + }, + + changed: false, + + saveChanges() { + const hash = this.getProperties.apply(this, arguments); + return this.save(hash) + .finally(() => this.set("changed", false)) + .catch(popupAjaxError); + }, + + saveSettings(name, value) { + const settings = {}; + settings[name] = value; + return this.save({ settings }); + }, + + saveTranslation(name, value) { + return this.save({ translations: { [name]: value } }); + } +}); + +export default Theme; diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 deleted file mode 100644 index 543858c677..0000000000 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ /dev/null @@ -1,352 +0,0 @@ -import { get } from "@ember/object"; -import { isEmpty } from "@ember/utils"; -import { or, gt } from "@ember/object/computed"; -import RestModel from "discourse/models/rest"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { ajax } from "discourse/lib/ajax"; -import { escapeExpression } from "discourse/lib/utilities"; -import highlightSyntax from "discourse/lib/highlight-syntax"; -import { url } from "discourse/lib/computed"; - -const THEME_UPLOAD_VAR = 2; -const FIELDS_IDS = [0, 1, 5]; - -export const THEMES = "themes"; -export const COMPONENTS = "components"; -const SETTINGS_TYPE_ID = 5; - -const Theme = RestModel.extend({ - isActive: or("default", "user_selectable"), - isPendingUpdates: gt("remote_theme.commits_behind", 0), - hasEditedFields: gt("editedFields.length", 0), - hasParents: gt("parent_themes.length", 0), - diffLocalChangesUrl: url("id", "/admin/themes/%@/diff_local_changes"), - - @discourseComputed("theme_fields.[]") - targets() { - return [ - { id: 0, name: "common" }, - { id: 1, name: "desktop", icon: "desktop" }, - { id: 2, name: "mobile", icon: "mobile-alt" }, - { id: 3, name: "settings", icon: "cog", advanced: true }, - { - id: 4, - name: "translations", - icon: "globe", - advanced: true, - customNames: true - }, - { - id: 5, - name: "extra_scss", - icon: "paint-brush", - advanced: true, - customNames: true - } - ].map(target => { - target["edited"] = this.hasEdited(target.name); - target["error"] = this.hasError(target.name); - return target; - }); - }, - - @discourseComputed("theme_fields.[]") - fieldNames() { - const common = [ - "scss", - "head_tag", - "header", - "after_header", - "body_tag", - "footer" - ]; - - const scss_fields = (this.theme_fields || []) - .filter(f => f.target === "extra_scss" && f.name !== "") - .map(f => f.name); - - if (scss_fields.length < 1) { - scss_fields.push("importable_scss"); - } - - return { - common: [...common, "embedded_scss"], - desktop: common, - mobile: common, - settings: ["yaml"], - translations: [ - "en", - ...(this.theme_fields || []) - .filter(f => f.target === "translations" && f.name !== "en") - .map(f => f.name) - ], - extra_scss: scss_fields - }; - }, - - @discourseComputed( - "fieldNames", - "theme_fields.[]", - "theme_fields.@each.error" - ) - fields(fieldNames) { - const hash = {}; - Object.keys(fieldNames).forEach(target => { - hash[target] = fieldNames[target].map(fieldName => { - const field = { - name: fieldName, - edited: this.hasEdited(target, fieldName), - error: this.hasError(target, fieldName) - }; - - if (target === "translations" || target === "extra_scss") { - field.translatedName = fieldName; - } else { - field.translatedName = I18n.t( - `admin.customize.theme.${fieldName}.text` - ); - field.title = I18n.t(`admin.customize.theme.${fieldName}.title`); - } - - if (fieldName.indexOf("_tag") > 0) { - field.icon = "far-file-alt"; - } - - return field; - }); - }); - return hash; - }, - - @discourseComputed("theme_fields") - themeFields(fields) { - if (!fields) { - this.set("theme_fields", []); - return {}; - } - - let hash = {}; - fields.forEach(field => { - if (!field.type_id || FIELDS_IDS.includes(field.type_id)) { - hash[this.getKey(field)] = field; - } - }); - return hash; - }, - - @discourseComputed("theme_fields", "theme_fields.[]") - uploads(fields) { - if (!fields) { - return []; - } - return fields.filter( - f => f.target === "common" && f.type_id === THEME_UPLOAD_VAR - ); - }, - - @discourseComputed("theme_fields", "theme_fields.@each.error") - isBroken(fields) { - return fields && fields.any(field => field.error && field.error.length > 0); - }, - - @discourseComputed("theme_fields.[]") - editedFields(fields) { - return fields.filter( - field => !Ember.isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID - ); - }, - - @discourseComputed("remote_theme.last_error_text") - remoteError(errorText) { - if (errorText && errorText.length > 0) { - return errorText; - } - }, - - getKey(field) { - return `${field.target} ${field.name}`; - }, - - hasEdited(target, name) { - if (name) { - return !isEmpty(this.getField(target, name)); - } else { - let fields = this.theme_fields || []; - return fields.any( - field => field.target === target && !isEmpty(field.value) - ); - } - }, - - hasError(target, name) { - return this.theme_fields - .filter(f => f.target === target && (!name || name === f.name)) - .any(f => f.error); - }, - - getError(target, name) { - let themeFields = this.themeFields; - let key = this.getKey({ target, name }); - let field = themeFields[key]; - return field ? field.error : ""; - }, - - getField(target, name) { - let themeFields = this.themeFields; - let key = this.getKey({ target, name }); - let field = themeFields[key]; - return field ? field.value : ""; - }, - - removeField(field) { - this.set("changed", true); - - field.upload_id = null; - field.value = null; - - return this.saveChanges("theme_fields"); - }, - - setField(target, name, value, upload_id, type_id) { - this.set("changed", true); - let themeFields = this.themeFields; - let field = { name, target, value, upload_id, type_id }; - - // slow path for uploads and so on - if (type_id && type_id > 1) { - let fields = this.theme_fields; - let existing = fields.find( - f => f.target === target && f.name === name && f.type_id === type_id - ); - if (existing) { - existing.value = value; - existing.upload_id = upload_id; - } else { - fields.pushObject(field); - } - return; - } - - // fast path - let key = this.getKey({ target, name }); - let existingField = themeFields[key]; - if (!existingField) { - this.theme_fields.pushObject(field); - themeFields[key] = field; - } else { - const changed = - (isEmpty(existingField.value) && !isEmpty(value)) || - (isEmpty(value) && !isEmpty(existingField.value)); - - existingField.value = value; - if (changed) { - // Observing theme_fields.@each.value is too slow, so manually notify - // if the value goes to/from blank - this.notifyPropertyChange("theme_fields.[]"); - } - } - }, - - @discourseComputed("childThemes.[]") - child_theme_ids(childThemes) { - if (childThemes) { - return childThemes.map(theme => get(theme, "id")); - } - }, - - removeChildTheme(theme) { - const childThemes = this.childThemes; - childThemes.removeObject(theme); - return this.saveChanges("child_theme_ids"); - }, - - addChildTheme(theme) { - let childThemes = this.childThemes; - if (!childThemes) { - childThemes = []; - this.set("childThemes", childThemes); - } - childThemes.removeObject(theme); - childThemes.pushObject(theme); - return this.saveChanges("child_theme_ids"); - }, - - addParentTheme(theme) { - let parentThemes = this.parentThemes; - if (!parentThemes) { - parentThemes = []; - this.set("parentThemes", parentThemes); - } - parentThemes.addObject(theme); - }, - - @discourseComputed("name", "default") - description: function(name, isDefault) { - if (isDefault) { - return I18n.t("admin.customize.theme.default_name", { name: name }); - } else { - return name; - } - }, - - checkForUpdates() { - return this.save({ remote_check: true }).then(() => - this.set("changed", false) - ); - }, - - updateToLatest() { - return ajax(this.diffLocalChangesUrl).then(json => { - if (json && json.error) { - bootbox.alert( - I18n.t("generic_error_with_reason", { - error: json.error - }) - ); - } else if (json && json.diff) { - bootbox.confirm( - I18n.t("admin.customize.theme.update_confirm") + - `
${escapeExpression(
-              json.diff
-            )}
`, - I18n.t("cancel"), - I18n.t("admin.customize.theme.update_confirm_yes"), - result => { - if (result) { - return this.save({ remote_update: true }).then(() => - this.set("changed", false) - ); - } - } - ); - highlightSyntax(); - } else { - return this.save({ remote_update: true }).then(() => - this.set("changed", false) - ); - } - }); - }, - - changed: false, - - saveChanges() { - const hash = this.getProperties.apply(this, arguments); - return this.save(hash) - .finally(() => this.set("changed", false)) - .catch(popupAjaxError); - }, - - saveSettings(name, value) { - const settings = {}; - settings[name] = value; - return this.save({ settings }); - }, - - saveTranslation(name, value) { - return this.save({ translations: { [name]: value } }); - } -}); - -export default Theme; diff --git a/app/assets/javascripts/admin/models/tl3-requirements.js.es6 b/app/assets/javascripts/admin/models/tl3-requirements.js similarity index 100% rename from app/assets/javascripts/admin/models/tl3-requirements.js.es6 rename to app/assets/javascripts/admin/models/tl3-requirements.js diff --git a/app/assets/javascripts/admin/models/user-field.js.es6 b/app/assets/javascripts/admin/models/user-field.js similarity index 100% rename from app/assets/javascripts/admin/models/user-field.js.es6 rename to app/assets/javascripts/admin/models/user-field.js diff --git a/app/assets/javascripts/admin/models/version-check.js.es6 b/app/assets/javascripts/admin/models/version-check.js similarity index 100% rename from app/assets/javascripts/admin/models/version-check.js.es6 rename to app/assets/javascripts/admin/models/version-check.js diff --git a/app/assets/javascripts/admin/models/watched-word.js.es6 b/app/assets/javascripts/admin/models/watched-word.js similarity index 100% rename from app/assets/javascripts/admin/models/watched-word.js.es6 rename to app/assets/javascripts/admin/models/watched-word.js diff --git a/app/assets/javascripts/admin/models/web-hook.js.es6 b/app/assets/javascripts/admin/models/web-hook.js similarity index 100% rename from app/assets/javascripts/admin/models/web-hook.js.es6 rename to app/assets/javascripts/admin/models/web-hook.js diff --git a/app/assets/javascripts/admin/routes/admin-api-index.js.es6 b/app/assets/javascripts/admin/routes/admin-api-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-api-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-api-index.js diff --git a/app/assets/javascripts/admin/routes/admin-api-keys-index.js.es6 b/app/assets/javascripts/admin/routes/admin-api-keys-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-api-keys-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-api-keys-index.js diff --git a/app/assets/javascripts/admin/routes/admin-api-keys-new.es6 b/app/assets/javascripts/admin/routes/admin-api-keys-new.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-api-keys-new.es6 rename to app/assets/javascripts/admin/routes/admin-api-keys-new.js diff --git a/app/assets/javascripts/admin/routes/admin-api-keys-show.js.es6 b/app/assets/javascripts/admin/routes/admin-api-keys-show.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-api-keys-show.js.es6 rename to app/assets/javascripts/admin/routes/admin-api-keys-show.js diff --git a/app/assets/javascripts/admin/routes/admin-api-keys.js.es6 b/app/assets/javascripts/admin/routes/admin-api-keys.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-api-keys.js.es6 rename to app/assets/javascripts/admin/routes/admin-api-keys.js diff --git a/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 b/app/assets/javascripts/admin/routes/admin-backups-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-backups-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-backups-index.js diff --git a/app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-backups-logs.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 rename to app/assets/javascripts/admin/routes/admin-backups-logs.js diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-backups.js.es6 rename to app/assets/javascripts/admin/routes/admin-backups.js diff --git a/app/assets/javascripts/admin/routes/admin-badges-award.js.es6 b/app/assets/javascripts/admin/routes/admin-badges-award.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-badges-award.js.es6 rename to app/assets/javascripts/admin/routes/admin-badges-award.js diff --git a/app/assets/javascripts/admin/routes/admin-badges-index.js.es6 b/app/assets/javascripts/admin/routes/admin-badges-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-badges-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-badges-index.js diff --git a/app/assets/javascripts/admin/routes/admin-badges-show.js b/app/assets/javascripts/admin/routes/admin-badges-show.js new file mode 100644 index 0000000000..4a29cfae0a --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-badges-show.js @@ -0,0 +1,63 @@ +import { get } from "@ember/object"; +import Route from "@ember/routing/route"; +import { ajax } from "discourse/lib/ajax"; +import Badge from "discourse/models/badge"; +import showModal from "discourse/lib/show-modal"; + +export default Route.extend({ + serialize(m) { + return { badge_id: get(m, "id") || "new" }; + }, + + model(params) { + if (params.badge_id === "new") { + return Badge.create({ + name: I18n.t("admin.badges.new_badge") + }); + } + return this.modelFor("adminBadges").findBy( + "id", + parseInt(params.badge_id, 10) + ); + }, + + actions: { + saveError(e) { + let msg = I18n.t("generic_error"); + if (e.responseJSON && e.responseJSON.errors) { + msg = I18n.t("generic_error_with_reason", { + error: e.responseJSON.errors.join(". ") + }); + } + bootbox.alert(msg); + }, + + editGroupings() { + const model = this.controllerFor("admin-badges").get("badgeGroupings"); + showModal("admin-edit-badge-groupings", { model, admin: true }); + }, + + preview(badge, explain) { + badge.set("preview_loading", true); + ajax("/admin/badges/preview.json", { + type: "POST", + data: { + sql: badge.get("query"), + target_posts: !!badge.get("target_posts"), + trigger: badge.get("trigger"), + explain + } + }) + .then(function(model) { + badge.set("preview_loading", false); + showModal("admin-badge-preview", { model, admin: true }); + }) + .catch(function(error) { + badge.set("preview_loading", false); + // eslint-disable-next-line no-console + console.error(error); + bootbox.alert("Network error"); + }); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 b/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 deleted file mode 100644 index 67acdcba58..0000000000 --- a/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 +++ /dev/null @@ -1,63 +0,0 @@ -import { get } from "@ember/object"; -import Route from "@ember/routing/route"; -import { ajax } from "discourse/lib/ajax"; -import Badge from "discourse/models/badge"; -import showModal from "discourse/lib/show-modal"; - -export default Route.extend({ - serialize(m) { - return { badge_id: get(m, "id") || "new" }; - }, - - model(params) { - if (params.badge_id === "new") { - return Badge.create({ - name: I18n.t("admin.badges.new_badge") - }); - } - return this.modelFor("adminBadges").findBy( - "id", - parseInt(params.badge_id, 10) - ); - }, - - actions: { - saveError(e) { - let msg = I18n.t("generic_error"); - if (e.responseJSON && e.responseJSON.errors) { - msg = I18n.t("generic_error_with_reason", { - error: e.responseJSON.errors.join(". ") - }); - } - bootbox.alert(msg); - }, - - editGroupings() { - const model = this.controllerFor("admin-badges").get("badgeGroupings"); - showModal("admin-edit-badge-groupings", { model, admin: true }); - }, - - preview(badge, explain) { - badge.set("preview_loading", true); - ajax("/admin/badges/preview.json", { - method: "post", - data: { - sql: badge.get("query"), - target_posts: !!badge.get("target_posts"), - trigger: badge.get("trigger"), - explain - } - }) - .then(function(model) { - badge.set("preview_loading", false); - showModal("admin-badge-preview", { model, admin: true }); - }) - .catch(function(error) { - badge.set("preview_loading", false); - // eslint-disable-next-line no-console - console.error(error); - bootbox.alert("Network error"); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-badges.js.es6 b/app/assets/javascripts/admin/routes/admin-badges.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-badges.js.es6 rename to app/assets/javascripts/admin/routes/admin-badges.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-colors-show.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-colors-show.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-colors-show.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-colors.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-colors.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-colors.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-style-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-style-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-email-style-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-email-style-index.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-style.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-style.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-email-style.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-email-style.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-templates.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-templates.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-email-templates.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-email-templates.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-index.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-robots-txt.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-robots-txt.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-robots-txt.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-robots-txt.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-themes-edit.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-themes-index.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-themes-show.js diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 rename to app/assets/javascripts/admin/routes/admin-customize-themes.js diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-general.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-general.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-dashboard-general.js.es6 rename to app/assets/javascripts/admin/routes/admin-dashboard-general.js diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-reports.js b/app/assets/javascripts/admin/routes/admin-dashboard-reports.js new file mode 100644 index 0000000000..b1eff77998 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-dashboard-reports.js @@ -0,0 +1,12 @@ +import DiscourseRoute from "discourse/routes/discourse"; +import { ajax } from "discourse/lib/ajax"; + +export default DiscourseRoute.extend({ + model() { + return ajax("/admin/reports"); + }, + + setupController(controller, model) { + controller.setProperties({ model: model.reports, filter: null }); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-reports.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-reports.js.es6 deleted file mode 100644 index 0de5bfe505..0000000000 --- a/app/assets/javascripts/admin/routes/admin-dashboard-reports.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; -import { ajax } from "discourse/lib/ajax"; - -export default DiscourseRoute.extend({ - model() { - return ajax("/admin/reports").then(json => json); - }, - - setupController(controller, model) { - controller.setProperties({ model: model.reports, filter: null }); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-dashboard.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-dashboard.js.es6 rename to app/assets/javascripts/admin/routes/admin-dashboard.js diff --git a/app/assets/javascripts/admin/routes/admin-email-bounced.js.es6 b/app/assets/javascripts/admin/routes/admin-email-bounced.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-bounced.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-bounced.js diff --git a/app/assets/javascripts/admin/routes/admin-email-incomings.js.es6 b/app/assets/javascripts/admin/routes/admin-email-incomings.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-incomings.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-incomings.js diff --git a/app/assets/javascripts/admin/routes/admin-email-index.js.es6 b/app/assets/javascripts/admin/routes/admin-email-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-index.js diff --git a/app/assets/javascripts/admin/routes/admin-email-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-email-logs.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-logs.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-logs.js diff --git a/app/assets/javascripts/admin/routes/admin-email-preview-digest.js.es6 b/app/assets/javascripts/admin/routes/admin-email-preview-digest.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-preview-digest.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-preview-digest.js diff --git a/app/assets/javascripts/admin/routes/admin-email-received.js.es6 b/app/assets/javascripts/admin/routes/admin-email-received.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-received.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-received.js diff --git a/app/assets/javascripts/admin/routes/admin-email-rejected.js.es6 b/app/assets/javascripts/admin/routes/admin-email-rejected.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-rejected.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-rejected.js diff --git a/app/assets/javascripts/admin/routes/admin-email-sent.js.es6 b/app/assets/javascripts/admin/routes/admin-email-sent.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-sent.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-sent.js diff --git a/app/assets/javascripts/admin/routes/admin-email-skipped.js.es6 b/app/assets/javascripts/admin/routes/admin-email-skipped.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-email-skipped.js.es6 rename to app/assets/javascripts/admin/routes/admin-email-skipped.js diff --git a/app/assets/javascripts/admin/routes/admin-embedding.js.es6 b/app/assets/javascripts/admin/routes/admin-embedding.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-embedding.js.es6 rename to app/assets/javascripts/admin/routes/admin-embedding.js diff --git a/app/assets/javascripts/admin/routes/admin-emojis.js.es6 b/app/assets/javascripts/admin/routes/admin-emojis.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-emojis.js.es6 rename to app/assets/javascripts/admin/routes/admin-emojis.js diff --git a/app/assets/javascripts/admin/routes/admin-flags-posts-active.js.es6 b/app/assets/javascripts/admin/routes/admin-flags-posts-active.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-flags-posts-active.js.es6 rename to app/assets/javascripts/admin/routes/admin-flags-posts-active.js diff --git a/app/assets/javascripts/admin/routes/admin-flags-posts-old.js.es6 b/app/assets/javascripts/admin/routes/admin-flags-posts-old.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-flags-posts-old.js.es6 rename to app/assets/javascripts/admin/routes/admin-flags-posts-old.js diff --git a/app/assets/javascripts/admin/routes/admin-flags-topics-index.js.es6 b/app/assets/javascripts/admin/routes/admin-flags-topics-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-flags-topics-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-flags-topics-index.js diff --git a/app/assets/javascripts/admin/routes/admin-logs-index.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-logs-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-logs-index.js diff --git a/app/assets/javascripts/admin/routes/admin-logs-screened-emails.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-screened-emails.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-logs-screened-emails.js.es6 rename to app/assets/javascripts/admin/routes/admin-logs-screened-emails.js diff --git a/app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js.es6 rename to app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js diff --git a/app/assets/javascripts/admin/routes/admin-logs-screened-urls.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-screened-urls.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-logs-screened-urls.js.es6 rename to app/assets/javascripts/admin/routes/admin-logs-screened-urls.js diff --git a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js new file mode 100644 index 0000000000..b007ecafa7 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js @@ -0,0 +1,29 @@ +import DiscourseRoute from "discourse/routes/discourse"; +import showModal from "discourse/lib/show-modal"; + +export default DiscourseRoute.extend({ + // TODO: make this automatic using an `{{outlet}}` + renderTemplate: function() { + this.render("admin/templates/logs/staff-action-logs", { + into: "adminLogs" + }); + }, + + activate() { + let controller = this.controllerFor("admin-logs-staff-action-logs"); + if (controller.filters === null) controller.resetFilters(); + }, + + actions: { + showDetailsModal(model) { + showModal("admin-staff-action-log-details", { model, admin: true }); + this.controllerFor("modal").set("modalClass", "log-details-modal"); + }, + + showCustomDetailsModal(model) { + let modal = showModal("admin-theme-change", { model, admin: true }); + this.controllerFor("modal").set("modalClass", "history-modal"); + modal.loadDiff(); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 deleted file mode 100644 index 0ef8e6e232..0000000000 --- a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; -import showModal from "discourse/lib/show-modal"; - -export default DiscourseRoute.extend({ - // TODO: make this automatic using an `{{outlet}}` - renderTemplate: function() { - this.render("admin/templates/logs/staff-action-logs", { - into: "adminLogs" - }); - }, - - actions: { - showDetailsModal(model) { - showModal("admin-staff-action-log-details", { model, admin: true }); - this.controllerFor("modal").set("modalClass", "log-details-modal"); - }, - - showCustomDetailsModal(model) { - let modal = showModal("admin-theme-change", { model, admin: true }); - this.controllerFor("modal").set("modalClass", "history-modal"); - modal.loadDiff(); - } - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-permalinks.js.es6 b/app/assets/javascripts/admin/routes/admin-permalinks.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-permalinks.js.es6 rename to app/assets/javascripts/admin/routes/admin-permalinks.js diff --git a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 b/app/assets/javascripts/admin/routes/admin-plugins.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-plugins.js.es6 rename to app/assets/javascripts/admin/routes/admin-plugins.js diff --git a/app/assets/javascripts/admin/routes/admin-reports-index.js.es6 b/app/assets/javascripts/admin/routes/admin-reports-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-reports-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-reports-index.js diff --git a/app/assets/javascripts/admin/routes/admin-reports-show.js.es6 b/app/assets/javascripts/admin/routes/admin-reports-show.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-reports-show.js.es6 rename to app/assets/javascripts/admin/routes/admin-reports-show.js diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-route-map.js.es6 rename to app/assets/javascripts/admin/routes/admin-route-map.js diff --git a/app/assets/javascripts/admin/routes/admin-search-logs-index.js.es6 b/app/assets/javascripts/admin/routes/admin-search-logs-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-search-logs-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-search-logs-index.js diff --git a/app/assets/javascripts/admin/routes/admin-search-logs-term.js.es6 b/app/assets/javascripts/admin/routes/admin-search-logs-term.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-search-logs-term.js.es6 rename to app/assets/javascripts/admin/routes/admin-search-logs-term.js diff --git a/app/assets/javascripts/admin/routes/admin-site-settings-category.js.es6 b/app/assets/javascripts/admin/routes/admin-site-settings-category.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-site-settings-category.js.es6 rename to app/assets/javascripts/admin/routes/admin-site-settings-category.js diff --git a/app/assets/javascripts/admin/routes/admin-site-settings-index.js.es6 b/app/assets/javascripts/admin/routes/admin-site-settings-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-site-settings-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-site-settings-index.js diff --git a/app/assets/javascripts/admin/routes/admin-site-settings.js.es6 b/app/assets/javascripts/admin/routes/admin-site-settings.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-site-settings.js.es6 rename to app/assets/javascripts/admin/routes/admin-site-settings.js diff --git a/app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text-edit.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-site-text-edit.js.es6 rename to app/assets/javascripts/admin/routes/admin-site-text-edit.js diff --git a/app/assets/javascripts/admin/routes/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/routes/admin-site-text-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-site-text-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-site-text-index.js diff --git a/app/assets/javascripts/admin/routes/admin-user-badges.js.es6 b/app/assets/javascripts/admin/routes/admin-user-badges.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-user-badges.js.es6 rename to app/assets/javascripts/admin/routes/admin-user-badges.js diff --git a/app/assets/javascripts/admin/routes/admin-user-fields.js.es6 b/app/assets/javascripts/admin/routes/admin-user-fields.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-user-fields.js.es6 rename to app/assets/javascripts/admin/routes/admin-user-fields.js diff --git a/app/assets/javascripts/admin/routes/admin-user-index.js.es6 b/app/assets/javascripts/admin/routes/admin-user-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-user-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-user-index.js diff --git a/app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js.es6 b/app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js.es6 rename to app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js diff --git a/app/assets/javascripts/admin/routes/admin-user.js.es6 b/app/assets/javascripts/admin/routes/admin-user.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-user.js.es6 rename to app/assets/javascripts/admin/routes/admin-user.js diff --git a/app/assets/javascripts/admin/routes/admin-users-index.js.es6 b/app/assets/javascripts/admin/routes/admin-users-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-users-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-users-index.js diff --git a/app/assets/javascripts/admin/routes/admin-users-list-index.js.es6 b/app/assets/javascripts/admin/routes/admin-users-list-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-users-list-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-users-list-index.js diff --git a/app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 b/app/assets/javascripts/admin/routes/admin-users-list-show.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 rename to app/assets/javascripts/admin/routes/admin-users-list-show.js diff --git a/app/assets/javascripts/admin/routes/admin-users-list.js.es6 b/app/assets/javascripts/admin/routes/admin-users-list.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-users-list.js.es6 rename to app/assets/javascripts/admin/routes/admin-users-list.js diff --git a/app/assets/javascripts/admin/routes/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words-action.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-watched-words-action.js.es6 rename to app/assets/javascripts/admin/routes/admin-watched-words-action.js diff --git a/app/assets/javascripts/admin/routes/admin-watched-words-index.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words-index.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-watched-words-index.js.es6 rename to app/assets/javascripts/admin/routes/admin-watched-words-index.js diff --git a/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-watched-words.js.es6 rename to app/assets/javascripts/admin/routes/admin-watched-words.js diff --git a/app/assets/javascripts/admin/routes/admin-web-hooks-show-events.js.es6 b/app/assets/javascripts/admin/routes/admin-web-hooks-show-events.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-web-hooks-show-events.js.es6 rename to app/assets/javascripts/admin/routes/admin-web-hooks-show-events.js diff --git a/app/assets/javascripts/admin/routes/admin-web-hooks-show.js.es6 b/app/assets/javascripts/admin/routes/admin-web-hooks-show.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-web-hooks-show.js.es6 rename to app/assets/javascripts/admin/routes/admin-web-hooks-show.js diff --git a/app/assets/javascripts/admin/routes/admin-web-hooks.js.es6 b/app/assets/javascripts/admin/routes/admin-web-hooks.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin-web-hooks.js.es6 rename to app/assets/javascripts/admin/routes/admin-web-hooks.js diff --git a/app/assets/javascripts/admin/routes/admin.js.es6 b/app/assets/javascripts/admin/routes/admin.js similarity index 100% rename from app/assets/javascripts/admin/routes/admin.js.es6 rename to app/assets/javascripts/admin/routes/admin.js diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js similarity index 100% rename from app/assets/javascripts/admin/services/admin-tools.js.es6 rename to app/assets/javascripts/admin/services/admin-tools.js diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index aec3433a0b..bcbe6940d4 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -1,45 +1,40 @@ {{#admin-wrapper class="container"}}
-
- -
- -
-
+
+
{{outlet}}
-
{{/admin-wrapper}} diff --git a/app/assets/javascripts/admin/templates/api-keys-index.hbs b/app/assets/javascripts/admin/templates/api-keys-index.hbs index 7943a5b9e2..ac5dbf7c6b 100644 --- a/app/assets/javascripts/admin/templates/api-keys-index.hbs +++ b/app/assets/javascripts/admin/templates/api-keys-index.hbs @@ -6,67 +6,66 @@ {{#if model}} - - - - - - - - - - {{#each model as |k|}} - - - - - - - - - {{/each}} - + + + + + + + + + + {{#each model as |k|}} + + + + + + + + + {{/each}} +
{{i18n "admin.api.key"}}{{i18n "admin.api.description"}}{{i18n "admin.api.user"}}{{i18n "admin.api.created"}}{{i18n "admin.api.last_used"}} 
- {{#if k.revoked_at}}{{d-icon 'times-circle'}}{{/if}} - {{k.truncatedKey}} - - {{k.shortDescription}} - -
{{i18n 'admin.api.user'}}
- {{#if k.user}} - {{#link-to "adminUser" k.user}} - {{avatar k.user imageSize="small"}} - {{/link-to}} - {{else}} - {{i18n "admin.api.all_users"}} - {{/if}} -
-
{{i18n 'admin.api.created'}}
- {{format-date k.created_at}} -
-
{{i18n 'admin.api.last_used'}}
- {{#if k.last_used_at}} - {{format-date k.last_used_at}} - {{else}} - {{i18n "admin.api.never_used"}} - {{/if}} -
- {{d-button action=(route-action "show" k) icon="far-eye" title="admin.api.show_details"}} - {{#if k.revoked_at}} - {{d-button - action=(action "undoRevokeKey") - actionParam=k icon="undo" - title="admin.api.undo_revoke"}} - {{else}} - {{d-button - class="btn-danger" - action=(action "revokeKey") - actionParam=k - icon="times" - title="admin.api.revoke"}} - {{/if}} - -
{{i18n "admin.api.key"}}{{i18n "admin.api.description"}}{{i18n "admin.api.user"}}{{i18n "admin.api.created"}}{{i18n "admin.api.last_used"}} 
+ {{#if k.revoked_at}}{{d-icon "times-circle"}}{{/if}} + {{k.truncatedKey}} + + {{k.shortDescription}} + +
{{i18n "admin.api.user"}}
+ {{#if k.user}} + {{#link-to "adminUser" k.user}} + {{avatar k.user imageSize="small"}} + {{/link-to}} + {{else}} + {{i18n "admin.api.all_users"}} + {{/if}} +
+
{{i18n "admin.api.created"}}
+ {{format-date k.created_at}} +
+
{{i18n "admin.api.last_used"}}
+ {{#if k.last_used_at}} + {{format-date k.last_used_at}} + {{else}} + {{i18n "admin.api.never_used"}} + {{/if}} +
+ {{d-button action=(route-action "show" k) icon="far-eye" title="admin.api.show_details"}} + {{#if k.revoked_at}} + {{d-button + action=(action "undoRevokeKey") + actionParam=k icon="undo" + title="admin.api.undo_revoke"}} + {{else}} + {{d-button + class="btn-danger" + action=(action "revokeKey") + actionParam=k + icon="times" + title="admin.api.revoke"}} + {{/if}} +
{{else}}

{{i18n "admin.api.none"}}

-{{/if}} \ No newline at end of file +{{/if}} diff --git a/app/assets/javascripts/admin/templates/api-keys-new.hbs b/app/assets/javascripts/admin/templates/api-keys-new.hbs index 3dac84dae2..d48da805e5 100644 --- a/app/assets/javascripts/admin/templates/api-keys-new.hbs +++ b/app/assets/javascripts/admin/templates/api-keys-new.hbs @@ -1,6 +1,6 @@ -{{#link-to 'adminApiKeys.index' class="go-back"}} - {{d-icon 'arrow-left'}} - {{i18n 'admin.api.all_api_keys'}} +{{#link-to "adminApiKeys.index" class="go-back"}} + {{d-icon "arrow-left"}} + {{i18n "admin.api.all_api_keys"}} {{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/api-keys-show.hbs b/app/assets/javascripts/admin/templates/api-keys-show.hbs index b96bc109b3..d1d35da017 100644 --- a/app/assets/javascripts/admin/templates/api-keys-show.hbs +++ b/app/assets/javascripts/admin/templates/api-keys-show.hbs @@ -1,11 +1,11 @@ -{{#link-to 'adminApiKeys.index' class="go-back"}} - {{d-icon 'arrow-left'}} - {{i18n 'admin.api.all_api_keys'}} +{{#link-to "adminApiKeys.index" class="go-back"}} + {{d-icon "arrow-left"}} + {{i18n "admin.api.all_api_keys"}} {{/link-to}}
{{#admin-form-row label="admin.api.key"}} - {{#if model.revoked_at}}{{d-icon 'times-circle'}}{{/if}} + {{#if model.revoked_at}}{{d-icon "times-circle"}}{{/if}} {{model.truncatedKey}} {{/admin-form-row}} @@ -18,7 +18,7 @@ {{/if}} -
+
{{#if editingDescription}} {{d-button class="ok" action=(action "saveDescription") icon="check"}} {{d-button class="cancel" action=(action "editDescription") icon="times"}} @@ -30,11 +30,11 @@ {{#admin-form-row label="admin.api.user"}} {{#if model.user}} - {{#link-to "adminUser" model.user}} - {{avatar model.user imageSize="small"}} {{model.user.username}} - {{/link-to}} + {{#link-to "adminUser" model.user}} + {{avatar model.user imageSize="small"}} {{model.user.username}} + {{/link-to}} {{else}} - {{i18n "admin.api.all_users"}} + {{i18n "admin.api.all_users"}} {{/if}} {{/admin-form-row}} @@ -58,7 +58,7 @@ {{#if model.revoked_at}} {{format-date model.revoked_at leaveAgo="true"}} {{/if}} -
+
{{#if model.revoked_at}} {{d-button action=(action "undoRevokeKey") diff --git a/app/assets/javascripts/admin/templates/api.hbs b/app/assets/javascripts/admin/templates/api.hbs index 45bfbd9ca7..288bbc71fe 100644 --- a/app/assets/javascripts/admin/templates/api.hbs +++ b/app/assets/javascripts/admin/templates/api.hbs @@ -1,6 +1,6 @@ {{#admin-nav}} - {{nav-item route='adminApiKeys' label='admin.api.title'}} - {{nav-item route='adminWebHooks' label='admin.web_hooks.title'}} + {{nav-item route="adminApiKeys" label="admin.api.title"}} + {{nav-item route="adminWebHooks" label="admin.web_hooks.title"}} {{/admin-nav}}
diff --git a/app/assets/javascripts/admin/templates/backups-index.hbs b/app/assets/javascripts/admin/templates/backups-index.hbs index 7f0675e5a5..8c5e593a63 100644 --- a/app/assets/javascripts/admin/templates/backups-index.hbs +++ b/app/assets/javascripts/admin/templates/backups-index.hbs @@ -41,57 +41,57 @@ - {{#each model as |backup|}} - - {{backup.filename}} - {{human-size backup.size}} - -
- {{d-button class="btn-default download" - action=(action "download") - actionParam=backup - icon="download" - title="admin.backups.operations.download.title" - label="admin.backups.operations.download.label"}} - {{#if status.isOperationRunning}} - {{d-button - icon="far-trash-alt" - action=(route-action "destroyBackup") - actionParam=backup class="btn-danger" - disabled="true" - title="admin.backups.operations.is_running"}} - {{d-button - icon="play" - action=(route-action "startRestore") - actionParam=backup disabled=status.restoreDisabled - class="btn-default" - title=restoreTitle - label="admin.backups.operations.restore.label"}} - {{else}} - {{d-button - icon="far-trash-alt" - action=(route-action "destroyBackup") - actionParam=backup - class="btn-danger" - title="admin.backups.operations.destroy.title"}} - {{d-button - icon="play" - action=(route-action "startRestore") - actionParam=backup - disabled=status.restoreDisabled - class="btn-default" - title=restoreTitle - label="admin.backups.operations.restore.label"}} - {{/if}} -
- - - {{else}} - - {{i18n "admin.backups.none"}} - - - - {{/each}} + {{#each model as |backup|}} + + {{backup.filename}} + {{human-size backup.size}} + +
+ {{d-button class="btn-default download" + action=(action "download") + actionParam=backup + icon="download" + title="admin.backups.operations.download.title" + label="admin.backups.operations.download.label"}} + {{#if status.isOperationRunning}} + {{d-button + icon="far-trash-alt" + action=(route-action "destroyBackup") + actionParam=backup class="btn-danger" + disabled="true" + title="admin.backups.operations.is_running"}} + {{d-button + icon="play" + action=(route-action "startRestore") + actionParam=backup disabled=status.restoreDisabled + class="btn-default" + title=restoreTitle + label="admin.backups.operations.restore.label"}} + {{else}} + {{d-button + icon="far-trash-alt" + action=(route-action "destroyBackup") + actionParam=backup + class="btn-danger" + title="admin.backups.operations.destroy.title"}} + {{d-button + icon="play" + action=(route-action "startRestore") + actionParam=backup + disabled=status.restoreDisabled + class="btn-default btn-restore" + title=restoreTitle + label="admin.backups.operations.restore.label"}} + {{/if}} +
+ + + {{else}} + + {{i18n "admin.backups.none"}} + + + + {{/each}} diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index c8d72599e8..23fb6dc458 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -1,9 +1,9 @@ -
+