diff --git a/.eslintignore b/.eslintignore index 1806ae7c09..e877e0062b 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/ @@ -13,3 +13,5 @@ vendor/ test/javascripts/test_helper.js test/javascripts/fixtures test/javascripts/helpers/assertions.js +node_modules/ +dist/ diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f1b56462f2..be7d476572 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -7,3 +7,24 @@ # DEV: enforces no self-closing-void-elements dafd3c3b47f116c6c1dc56cb18df614c11747733 + +# Rename many `.js.es6` files to `.js` +032205e2029cbf82dc8f05b459fb93adf2503c60 + +# Rename pretty-text from es6 -> js +c15056650647e8650288f973d9038500dc9cf7bb + +# Rename wizard from es6 -> js +1ac02422011f89716ab27250d39b0e0212e03892 + +# DEV: enforces block-indentation of ember-template-lint rules +b66b277dc44bcd2122dc21965dab209c30636214 + +# DEV: enforces double quotes ember-template-lint +c4644c61d97c823b7dd940ffaf0967a104f4b58c + +# Migrate to app directory +7a2e8d3ead63c7d99e1069fc7823e933f931ba85 + +# DEV: Fix indentation for routes.rb +985900818ff985b04def6aa4c5d99c1aa6dbd45c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02b8d1947..04a646d9d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - master pull_request: branches-ignore: - - 'tests-passed' + - "tests-passed" jobs: build: @@ -28,12 +28,12 @@ jobs: fail-fast: false matrix: - build_types: [ 'BACKEND', 'FRONTEND', 'LINT' ] - target: [ 'PLUGINS', 'CORE' ] - os: [ ubuntu-latest ] - ruby: [ '2.6' ] - postgres: [ '10' ] - redis: [ '4.x' ] + build_types: ["BACKEND", "FRONTEND", "LINT"] + target: ["PLUGINS", "CORE"] + os: [ubuntu-latest] + ruby: ["2.6"] + postgres: ["12"] + redis: ["4.x"] services: postgres: @@ -57,14 +57,24 @@ jobs: fetch-depth: 1 - name: Setup Git - run: git config --global user.email "ci@ci.invalid" && git config --global user.name "Discourse CI" + run: | + git config --global user.email "ci@ci.invalid" + git config --global user.name "Discourse CI" - name: Setup packages if: env.BUILD_TYPE != 'LINT' run: | - sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead && \ + sudo apt-get update + sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh + - name: Update imagemagick + if: env.BUILD_TYPE == 'BACKEND' + run: | + wget https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-imagemagick + chmod +x install-imagemagick + sudo ./install-imagemagick + - name: Setup redis uses: shogo82148/actions-setup-redis@v1 if: env.BUILD_TYPE != 'LINT' @@ -75,10 +85,12 @@ jobs: uses: actions/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - architecture: 'x64' - name: Setup bundler - run: gem install bundler -v 2.1.1 --no-doc + run: | + gem install bundler -v 2.1.4 --no-doc + bundle config deployment 'true' + bundle config without 'development' - name: Bundler cache uses: actions/cache@v1 @@ -90,7 +102,7 @@ jobs: ${{ runner.os }}-gem- - name: Setup gems - run: bundle install --without development --deployment --jobs 4 --retry 3 + run: bundle install --jobs 4 - name: Get yarn cache directory id: yarn-cache-dir @@ -114,11 +126,15 @@ jobs: - name: Create database if: env.BUILD_TYPE != 'LINT' - run: bin/rake db:create && bin/rake db:migrate + run: | + bin/rake db:create + bin/rake db:migrate - name: Create parallel databases if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE' - run: bin/rake parallel:create && bin/rake parallel:migrate + run: | + bin/rake parallel:create + bin/rake parallel:migrate - name: Rubocop if: env.BUILD_TYPE == 'LINT' @@ -126,17 +142,29 @@ jobs: - name: ESLint if: env.BUILD_TYPE == 'LINT' - run: yarn eslint app/assets/javascripts test/javascripts && yarn eslint --ext .es6 app/assets/javascripts test/javascripts plugins + run: | + yarn eslint app/assets/javascripts test/javascripts + yarn eslint --global I18n --ext .es6 plugins - name: Prettier 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 English locale + if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE' + run: bundle exec ruby script/i18n_lint.rb "config/**/locales/{client,server}.en.yml" + + - name: Plugin English locale + if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS' + run: bundle exec ruby script/i18n_lint.rb "plugins/**/locales/{client,server}.en.yml" - name: Core RSpec if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE' - run: bin/turbo_rspec && bin/rake plugin:spec + run: | + bin/turbo_rspec + bin/rake plugin:spec - name: Plugin RSpec if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'PLUGINS' @@ -154,5 +182,5 @@ jobs: - name: Plugin QUnit # Tests core plugins in TARGET=CORE, and all plugins in TARGET=PLUGINS if: env.BUILD_TYPE == 'FRONTEND' - run: bundle exec rake plugin:qunit + run: bundle exec rake plugin:qunit['*','1200000'] timeout-minutes: 30 diff --git a/.gitignore b/.gitignore index 59ad3c0b6f..2ef99613b6 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ bootsnap-compile-cache/ !/plugins/discourse-narrative-bot !/plugins/discourse-presence !/plugins/discourse-local-dates -!/plugins/discourse-internet-explorer +!/plugins/discourse-unsupported-browser /plugins/*/auto_generated/ /spec/fixtures/plugins/my_plugin/auto_generated @@ -122,7 +122,7 @@ vendor/bundle/* *.swn # ignore nodejs files -/node_modules +node_modules /package-lock.json /vendor/data/GeoLite2-City.mmdb @@ -132,3 +132,12 @@ vendor/bundle/* # ignore auto-generated plugin js assets /app/assets/javascripts/plugins/* + +# ignore generated api documentation files +openapi/* + +# ember-cli generated +dist + +# Copyright Deposits +copyright diff --git a/.rubocop.yml b/.rubocop.yml index 1e682c1045..b7edfe4894 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,146 +1,9 @@ -require: - - rubocop-discourse +inherit_gem: + rubocop-discourse: default.yml -AllCops: - TargetRubyVersion: 2.4 - DisabledByDefault: true - Exclude: - - "db/schema.rb" - - "bundle/**/*" - - "vendor/**/*" - - "node_modules/**/*" - - "public/**/*" - - "plugins/**/gems/**/*" - -# Prefer &&/|| over and/or. -Style/AndOr: - Enabled: true - -Style/FrozenStringLiteralComment: - Enabled: true - -# Align `when` with `case`. -Layout/CaseIndentation: - Enabled: true - -# Align comments with method definitions. -Layout/CommentIndentation: - Enabled: true - -# No extra empty lines. -Layout/EmptyLines: - Enabled: true - -# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. -Style/HashSyntax: - Enabled: true - -# Two spaces, no tabs (for indentation). -Layout/IndentationWidth: - Enabled: true - -Layout/SpaceAfterColon: - Enabled: true - -Layout/SpaceAfterComma: - Enabled: true - -Layout/SpaceAroundEqualsInParameterDefault: - Enabled: true - -Layout/SpaceAroundKeyword: - Enabled: true - -Layout/SpaceAroundOperators: - Enabled: true - -Layout/SpaceBeforeFirstArg: - Enabled: true - -# Defining a method with parameters needs parentheses. -Style/MethodDefParentheses: - Enabled: true - -# Use `foo {}` not `foo{}`. -Layout/SpaceBeforeBlockBraces: - Enabled: true - -# Use `foo { bar }` not `foo {bar}`. -Layout/SpaceInsideBlockBraces: - Enabled: true - -# Use `{ a: 1 }` not `{a:1}`. -Layout/SpaceInsideHashLiteralBraces: - Enabled: true - -Layout/SpaceInsideParens: - Enabled: true - -# Detect hard tabs, no hard tabs. -Layout/Tab: - Enabled: true - -# Blank lines should not have any spaces. -Layout/TrailingEmptyLines: - Enabled: true - -# No trailing whitespace. -Layout/TrailingWhitespace: - Enabled: true - -Lint/Debugger: - Enabled: true - -Layout/BlockAlignment: - Enabled: true - -# Align `end` with the matching keyword or starting expression except for -# assignments, where it should be aligned with the LHS. -Layout/EndAlignment: - Enabled: true - EnforcedStyleAlignWith: variable - -# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. -Lint/RequireParentheses: - Enabled: true - -Lint/ShadowingOuterLocalVariable: - Enabled: true - -Layout/MultilineMethodCallIndentation: - Enabled: true - EnforcedStyle: indented - -Layout/HashAlignment: - Enabled: true - -Bundler/OrderedGems: +# Still work to do in ensuring we don't link old files +Discourse/NoAddReferenceOrAliasesActiveRecordMigration: Enabled: false -Style/SingleLineMethods: +Discourse/NoResetColumnInformationInMigrations: Enabled: true - -Style/Semicolon: - Enabled: true - AllowAsExpressionSeparator: true - -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 - Exclude: - - 'lib/tasks/**/*' - - 'script/**/*' - - 'spec/**/*.rb' - - 'plugins/*/spec/**/*' diff --git a/.template-lintrc.js b/.template-lintrc.js index 5731778fc0..adf0ccad1f 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -1,11 +1,55 @@ module.exports = { - // extends: "recommended", + extends: "recommended", ignore: ["**/*.raw"], + // Pending: + // "eol-last": "always", + rules: { + "block-indentation": true, + "deprecated-render-helper": true, + "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/.travis.yml b/.travis.yml deleted file mode 100644 index 7119b6cd2e..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,86 +0,0 @@ -language: ruby - -git: - depth: false - -branches: - only: - - master - - beta - - stable - -env: - global: - - TRAVIS_NODE_VERSION="10" - - DISCOURSE_HOSTNAME=www.example.com - - RUBY_GLOBAL_METHOD_CACHE_SIZE=131072 - matrix: - - "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0" - - "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0" - - "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=1" - -addons: - chrome: stable - postgresql: "9.6" - apt: - update: true - packages: - - gifsicle - - jpegoptim - - optipng - - jhead - -matrix: - fast_finish: true - -rvm: - - 2.6.3 - -services: - - redis-server - -sudo: required -dist: xenial - -cache: - yarn: true - directories: - - vendor/bundle - -before_install: - - wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh - - nvm install node - - node --version - - gem install bundler -v 1.17.3 - - git clone --depth=1 https://github.com/discourse/discourse-backup-uploads-to-s3.git plugins/discourse-backup-uploads-to-s3 - - git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert - - git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday - - git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies - - git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration - - git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign - - git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon - - git clone --depth=1 https://github.com/discourse/discourse-user-notes.git plugins/discourse-user-notes - - git clone --depth=1 https://github.com/discourse/discourse-group-tracker - - export PATH=$HOME/.yarn/bin:$PATH - -install: - - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi" - - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi" - - bash -c "if [ '$QUNIT_RUN' == '1' ] || [ '$RUN_LINT' == '1' ]; then yarn install --dev; fi" - - bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create && LOAD_PLUGINS=1 bundle exec rake db:migrate; fi" - -script: - - | - bash -c " - if [ '$RUN_LINT' == '1' ]; then - npx lefthook run lints - else - if [ '$QUNIT_RUN' == '1' ]; then - bundle exec rake qunit:test['1200000'] && \ - bundle exec rake qunit:test['1200000','/wizard/qunit'] && \ - bundle exec rake plugin:qunit - else - bundle exec rspec && bundle exec rake plugin:spec - fi - fi - " 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/COPYRIGHT.txt b/COPYRIGHT.txt index b22e221ed4..cb867e5604 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -29,5 +29,3 @@ Javascript Ruby Rails - Copyright (c) 2005-2013 David Heinemeier Hansson, Rails Core Team contributors (MIT) - - Thin - Copyright (c) 2012-2013 Marc-Andre Cournoyer 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..4d809fda20 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.3.1' + gem 'actionpack', '6.0.3.1' + gem 'actionview', '6.0.3.1' + gem 'activemodel', '6.0.3.1' + gem 'activerecord', '6.0.3.1' + gem 'activesupport', '6.0.3.1' + gem 'railties', '6.0.3.1' 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' @@ -110,30 +112,23 @@ gem 'oj' gem 'pg' gem 'mini_sql' gem 'pry-rails', require: false +gem 'pry-byebug', require: false gem 'r2', require: false gem 'rake' gem 'thor', require: false gem 'diffy', require: false gem 'rinku' -gem 'sanitize' gem 'sidekiq' gem 'mini_scheduler' -# for sidekiq web -gem 'tilt', require: false - gem 'execjs', require: false gem 'mini_racer' # TODO: determine why highline is being held back and upgrade to latest gem 'highline', '~> 1.7.0', require: false -# TODO: Upgrading breaks Sidekiq Web -# This is a bit of a hornets nest cause in an ideal world we much prefer -# if Sidekiq reused session and CSRF mitigation with Discourse on the -# _forum_session cookie instead of a rack.session cookie -gem 'rack', '2.0.8' +gem 'rack', '2.2.2' gem 'rack-protection' # security gem 'cbor', require: false @@ -161,10 +156,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 @@ -172,22 +164,21 @@ group :test, :development do # we would like to upgrade it if possible gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false - # TODO once 4.0.0 is released upgrade to it, at time of writing 3.9.0 is latest - gem 'rspec-rails', '4.0.0.beta2', require: false + gem 'rspec-rails' gem 'shoulda-matchers', require: false gem 'rspec-html-matchers' - gem 'pry-nav' gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri - gem 'rubocop', require: false gem "rubocop-discourse", require: false gem 'parallel_tests' + + gem 'rswag-specs' end group :development do gem 'ruby-prof', require: false, platform: :mri gem 'bullet', require: !!ENV['BULLET'] - gem 'better_errors', platform: :mri + gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS'] gem 'binding_of_caller' gem 'yaml-lint' gem 'annotate' @@ -208,7 +199,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 +249,5 @@ end gem 'webpush', require: false gem 'colored2', require: false gem 'maxminddb' + +gem 'rails_failover', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 242ce594b0..4862c4a3be 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.3.1) + actionpack (= 6.0.3.1) + actionview (= 6.0.3.1) + activejob (= 6.0.3.1) 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.3.1) + actionview (= 6.0.3.1) + activesupport (= 6.0.3.1) + 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.3.1) + activesupport (= 6.0.3.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -24,49 +24,49 @@ 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.3.1) + activesupport (= 6.0.3.1) 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.3.1) + activesupport (= 6.0.3.1) + activerecord (6.0.3.1) + activemodel (= 6.0.3.1) + activesupport (= 6.0.3.1) + activesupport (6.0.3.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - zeitwerk (~> 2.2) + zeitwerk (~> 2.2, >= 2.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) + ast (2.4.1) + aws-eventstream (1.1.0) + aws-partitions (1.329.0) + aws-sdk-core (3.99.1) + 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.31.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.60.2) - aws-sdk-core (~> 3, >= 3.83.0) + aws-sdk-s3 (1.66.0) + aws-sdk-core (~> 3, >= 3.96.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sdk-sns (1.21.0) - aws-sdk-core (~> 3, >= 3.71.0) + aws-sdk-sns (1.25.1) + aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.0) - aws-eventstream (~> 1.0, >= 1.0.2) + aws-sigv4 (1.2.0) + aws-eventstream (~> 1, >= 1.0.2) barber (0.12.2) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) - better_errors (2.5.1) + better_errors (2.7.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -78,17 +78,17 @@ GEM bullet (6.1.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - byebug (11.1.1) + byebug (11.1.3) cbor (0.5.9.6) certified (1.0.0) chunky_png (1.3.11) - coderay (1.1.2) + coderay (1.1.3) colored2 (3.1.2) concurrent-ruby (1.1.6) - connection_pool (2.2.2) - cose (0.11.0) + connection_pool (2.2.3) + 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) @@ -96,7 +96,7 @@ GEM css_parser (1.7.1) addressable debug_inspector (0.0.3) - diff-lcs (1.3) + diff-lcs (1.4.1) diffy (3.3.0) discourse-ember-source (3.12.2.0) discourse_image_optim (0.26.2) @@ -106,7 +106,7 @@ GEM in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) docile (1.3.2) - email_reply_trimmer (0.1.12) + email_reply_trimmer (0.1.13) ember-data-source (3.0.2) ember-source (>= 2, < 3.0) ember-handlebars-template (0.8.0) @@ -121,12 +121,12 @@ GEM railties (>= 3.1) ember-source (2.18.2) erubi (1.9.0) - excon (0.72.0) + excon (0.75.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) + faraday (1.0.1) multipart-post (>= 1.2, < 3) fast_blank (1.0.0) fast_xor (1.1.3) @@ -134,29 +134,31 @@ GEM rake-compiler fast_xs (0.8.0) fastimage (2.1.7) - ffi (1.12.2) + ffi (1.13.1) flamegraph (0.9.5) fspath (3.1.2) gc_tracer (1.5.1) globalid (0.4.2) activesupport (>= 4.2.0) guess_html_encoding (0.0.11) - hashdiff (1.0.0) - hashie (3.6.0) + hashdiff (1.0.1) + hashie (4.1.0) highline (1.7.10) hkdf (0.3.0) htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.8.2) + i18n (1.8.3) concurrent-ruby (~> 1.0) image_size (1.5.0) in_threads (1.5.4) - jaro_winkler (1.5.4) jmespath (1.4.0) - jquery-rails (4.3.5) + jquery-rails (4.4.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + json (2.3.0) + json-schema (2.8.1) + addressable (>= 2.4) jwt (2.2.1) kgio (2.11.3) libv8 (7.3.492.27.1) @@ -171,8 +173,8 @@ GEM logstash-event (1.2.02) logstash-logger (0.26.1) logstash-event (~> 1.2) - logster (2.6.3) - loofah (2.4.0) + logster (2.9.0) + loofah (2.6.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) @@ -181,43 +183,41 @@ GEM mini_mime (>= 0.1.1) maxminddb (0.1.22) memory_profiler (0.9.14) - message_bus (2.2.3) + message_bus (3.3.1) rack (>= 1.1.3) - metaclass (0.0.4) - method_source (0.9.2) + method_source (1.0.0) mini_mime (1.0.2) mini_portile2 (2.4.0) - mini_racer (0.2.9) - libv8 (>= 6.9.411) + mini_racer (0.2.14) + 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) + minitest (5.14.1) + mocha (1.11.2) + mock_redis (0.24.0) msgpack (1.3.3) multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) mustache (1.1.1) nio4r (2.5.2) - nokogiri (1.10.8) + nokogiri (1.10.9) mini_portile2 (~> 2.4.0) 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.2) - 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,35 +240,36 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.9.27.2) + onebox (1.9.29) 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) - parallel (1.19.1) - parallel_tests (2.31.0) + openssl-signature_algorithm (0.4.0) + optimist (3.0.1) + parallel (1.19.2) + parallel_tests (3.0.0) parallel - parser (2.7.0.2) - ast (~> 2.4.0) - pg (1.2.2) + parser (2.7.1.4) + ast (~> 2.4.1) + pg (1.2.3) progress (3.5.2) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-nav (0.3.0) - pry (>= 0.9.10, < 0.13.0) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.3) - puma (4.3.1) + public_suffix (4.0.5) + puma (4.3.5) nio4r (~> 2.0) r2 (0.2.7) - rack (2.0.8) - rack-mini-profiler (1.1.6) + rack (2.2.2) + rack-mini-profiler (2.0.2) rack (>= 1.2.0) rack-protection (2.0.8.1) rack @@ -279,12 +280,15 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - rails_multisite (2.0.7) - activerecord (> 4.2, < 7) - railties (> 4.2, < 7) - railties (6.0.1) - actionpack (= 6.0.1) - activesupport (= 6.0.1) + rails_failover (0.5.2) + activerecord (~> 6.0) + railties (~> 6.0) + rails_multisite (2.3.0) + activerecord (> 5.0, < 7) + railties (> 5.0, < 7) + railties (6.0.3.1) + actionpack (= 6.0.3.1) + activesupport (= 6.0.3.1) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -293,17 +297,18 @@ GEM rake (13.0.1) rake-compiler (1.1.0) rake - rb-fsevent (0.10.3) + rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) - rbtrace (0.4.11) + rbtrace (0.4.13) ffi (>= 1.0.6) msgpack (>= 0.4.3) optimist (>= 3.0.0) rchardet (1.8.0) - redis (4.1.3) + redis (4.2.1) redis-namespace (1.7.0) redis (>= 3.0.4) + regexp_parser (1.7.1) request_store (1.5.0) rack (>= 1.4) rexml (3.2.4) @@ -313,14 +318,14 @@ GEM rqrcode (1.1.2) chunky_png (~> 1.0) rqrcode_core (~> 0.1) - rqrcode_core (0.1.1) + rqrcode_core (0.1.2) rspec (3.9.0) rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) rspec-mocks (~> 3.9.0) - rspec-core (3.9.1) - rspec-support (~> 3.9.1) - rspec-expectations (3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-html-matchers (0.9.2) @@ -329,34 +334,44 @@ GEM rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) - rspec-rails (4.0.0.beta2) + rspec-rails (4.0.1) actionpack (>= 4.2) activesupport (>= 4.2) railties (>= 4.2) - rspec-core (~> 3.8) - rspec-expectations (~> 3.8) - rspec-mocks (~> 3.8) - rspec-support (~> 3.8) - rspec-support (3.9.2) + rspec-core (~> 3.9) + rspec-expectations (~> 3.9) + rspec-mocks (~> 3.9) + rspec-support (~> 3.9) + rspec-support (3.9.3) + rswag-specs (2.3.1) + activesupport (>= 3.1, < 7.0) + json-schema (~> 2.2) + railties (>= 3.1, < 7.0) rtlit (0.0.5) - rubocop (0.80.0) - jaro_winkler (~> 1.5.1) + rubocop (0.86.0) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.7) rexml + rubocop-ast (>= 0.0.3, < 1.0) 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-ast (0.0.3) + parser (>= 2.7.0.1) + rubocop-discourse (2.2.0) rubocop (>= 0.69.0) - ruby-prof (1.3.0) + rubocop-rspec (>= 1.39.0) + rubocop-rspec (1.40.0) + rubocop (>= 0.68.1) + ruby-prof (1.4.1) 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) + sanitize (5.2.1) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) @@ -374,15 +389,15 @@ 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) redis (>= 4.1.0) - simplecov (0.18.3) + simplecov (0.18.5) docile (~> 1.1) simplecov-html (~> 0.11) - simplecov-html (0.12.1) + simplecov-html (0.12.2) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -396,19 +411,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.5) 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 +431,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.3.1) + actionpack (= 6.0.3.1) + actionview (= 6.0.3.1) actionview_precompiler active_model_serializers (~> 0.8.3) - activemodel (= 6.0.1) - activerecord (= 6.0.1) - activesupport (= 6.0.1) + activemodel (= 6.0.3.1) + activerecord (= 6.0.3.1) + activesupport (= 6.0.3.1) addressable annotate aws-sdk-s3 @@ -465,6 +480,7 @@ DEPENDENCIES highline (~> 1.7.0) htmlentities http_accept_language + json listen lograge logstash-event @@ -482,7 +498,7 @@ DEPENDENCIES mini_sql mini_suffix minitest - mocha (= 1.8.0) + mocha mock_redis multi_json mustache @@ -498,15 +514,16 @@ DEPENDENCIES onebox parallel_tests pg - pry-nav + pry-byebug pry-rails puma r2 - rack (= 2.0.8) + rack (= 2.2.2) rack-mini-profiler rack-protection + rails_failover rails_multisite - railties (= 6.0.1) + railties (= 6.0.3.1) rake rb-fsevent rb-inotify (~> 0.9) @@ -519,14 +536,13 @@ DEPENDENCIES rqrcode rspec rspec-html-matchers - rspec-rails (= 4.0.0.beta2) + rspec-rails + rswag-specs rtlit - rubocop rubocop-discourse ruby-prof ruby-readability rubyzip - sanitize sassc (= 2.0.1) sassc-rails seed-fu @@ -539,7 +555,6 @@ DEPENDENCIES stackprof test-prof thor - tilt uglifier unf unicorn @@ -548,4 +563,4 @@ DEPENDENCIES yaml-lint BUNDLED WITH - 2.1.1 + 2.1.4 diff --git a/README.md b/README.md index 323a21e73b..fce15c577d 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 @@ -66,7 +66,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https ## Contributing -[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse) +[![Build Status](https://github.com/discourse/discourse/workflows/CI/badge.svg)](https://github.com/discourse/discourse/actions) Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that accepts contributions from the public – including you! @@ -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 b/app/assets/javascripts/admin/components/admin-backups-logs.js new file mode 100644 index 0000000000..76c8cff1ac --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-backups-logs.js @@ -0,0 +1,74 @@ +import I18n from "I18n"; +import { scheduleOnce } from "@ember/runloop"; +import Component from "@ember/component"; +import discourseDebounce from "discourse/lib/debounce"; +import { observes, on } from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNames: ["admin-backups-logs"], + showLoadingSpinner: false, + hasFormattedLogs: false, + noLogsMessage: I18n.t("admin.backups.logs.none"), + + init() { + this._super(...arguments); + this._reset(); + }, + + _reset() { + this.setProperties({ formattedLogs: "", index: 0 }); + }, + + _scrollDown() { + const div = this.element; + div.scrollTop = div.scrollHeight; + }, + + @on("init") + @observes("logs.[]") + _resetFormattedLogs() { + if (this.logs.length === 0) { + this._reset(); // reset the cached logs whenever the model is reset + this.renderLogs(); + } + }, + + @on("init") + @observes("logs.[]") + _updateFormattedLogs: discourseDebounce(function() { + const logs = this.logs; + if (logs.length === 0) return; + + // do the log formatting only once for HELLish performance + let formattedLogs = this.formattedLogs; + for (let i = this.index, length = logs.length; i < length; i++) { + const date = logs[i].get("timestamp"), + message = logs[i].get("message"); + formattedLogs += "[" + date + "] " + message + "\n"; + } + // update the formatted logs & cache index + this.setProperties({ + formattedLogs: formattedLogs, + index: logs.length + }); + // force rerender + this.renderLogs(); + + scheduleOnce("afterRender", this, this._scrollDown); + }, 150), + + renderLogs() { + const formattedLogs = this.formattedLogs; + if (formattedLogs && formattedLogs.length > 0) { + this.set("hasFormattedLogs", true); + } else { + this.set("hasFormattedLogs", false); + } + // add a loading indicator + if (this.get("status.isOperationRunning")) { + this.set("showLoadingSpinner", true); + } else { + this.set("showLoadingSpinner", false); + } + } +}); diff --git a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 deleted file mode 100644 index 1837409c58..0000000000 --- a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 +++ /dev/null @@ -1,73 +0,0 @@ -import { scheduleOnce } from "@ember/runloop"; -import Component from "@ember/component"; -import discourseDebounce from "discourse/lib/debounce"; -import { observes, on } from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNames: ["admin-backups-logs"], - showLoadingSpinner: false, - hasFormattedLogs: false, - noLogsMessage: I18n.t("admin.backups.logs.none"), - - init() { - this._super(...arguments); - this._reset(); - }, - - _reset() { - this.setProperties({ formattedLogs: "", index: 0 }); - }, - - _scrollDown() { - const div = this.element; - div.scrollTop = div.scrollHeight; - }, - - @on("init") - @observes("logs.[]") - _resetFormattedLogs() { - if (this.logs.length === 0) { - this._reset(); // reset the cached logs whenever the model is reset - this.renderLogs(); - } - }, - - @on("init") - @observes("logs.[]") - _updateFormattedLogs: discourseDebounce(function() { - const logs = this.logs; - if (logs.length === 0) return; - - // do the log formatting only once for HELLish performance - let formattedLogs = this.formattedLogs; - for (let i = this.index, length = logs.length; i < length; i++) { - const date = logs[i].get("timestamp"), - message = logs[i].get("message"); - formattedLogs += "[" + date + "] " + message + "\n"; - } - // update the formatted logs & cache index - this.setProperties({ - formattedLogs: formattedLogs, - index: logs.length - }); - // force rerender - this.renderLogs(); - - scheduleOnce("afterRender", this, this._scrollDown); - }, 150), - - renderLogs() { - const formattedLogs = this.formattedLogs; - if (formattedLogs && formattedLogs.length > 0) { - this.set("hasFormattedLogs", true); - } else { - this.set("hasFormattedLogs", false); - } - // add a loading indicator - if (this.get("status.isOperationRunning")) { - this.set("showLoadingSpinner", true); - } else { - this.set("showLoadingSpinner", false); - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 b/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 deleted file mode 100644 index 093f29826b..0000000000 --- a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import Component from "@ember/component"; -import { iconHTML } from "discourse-common/lib/icon-library"; - -export default Component.extend({ - tagName: "th", - classNames: ["sortable"], - chevronIcon: null, - toggleProperties() { - if (this.order === this.field) { - this.set("ascending", this.ascending ? null : true); - } else { - this.setProperties({ order: this.field, ascending: null }); - } - }, - toggleChevron() { - if (this.order === this.field) { - let chevron = iconHTML(this.ascending ? "chevron-up" : "chevron-down"); - this.set("chevronIcon", `${chevron}`.htmlSafe()); - } else { - this.set("chevronIcon", null); - } - }, - click() { - this.toggleProperties(); - }, - didReceiveAttrs() { - this._super(...arguments); - this.toggleChevron(); - } -}); 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 b/app/assets/javascripts/admin/components/admin-report-storage-stats.js new file mode 100644 index 0000000000..0be7256690 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report-storage-stats.js @@ -0,0 +1,43 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; +import Component from "@ember/component"; +import { setting } from "discourse/lib/computed"; + +export default Component.extend({ + classNames: ["admin-report-storage-stats"], + + backupLocation: setting("backup_location"), + backupStats: alias("model.data.backups"), + uploadStats: alias("model.data.uploads"), + + @discourseComputed("backupStats") + showBackupStats(stats) { + return stats && this.currentUser.admin; + }, + + @discourseComputed("backupLocation") + backupLocationName(backupLocation) { + return I18n.t(`admin.backups.location.${backupLocation}`); + }, + + @discourseComputed("backupStats.used_bytes") + usedBackupSpace(bytes) { + return I18n.toHumanSize(bytes); + }, + + @discourseComputed("backupStats.free_bytes") + freeBackupSpace(bytes) { + return I18n.toHumanSize(bytes); + }, + + @discourseComputed("uploadStats.used_bytes") + usedUploadSpace(bytes) { + return I18n.toHumanSize(bytes); + }, + + @discourseComputed("uploadStats.free_bytes") + freeUploadSpace(bytes) { + return I18n.toHumanSize(bytes); + } +}); 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.es6 deleted file mode 100644 index 61629c626e..0000000000 --- a/app/assets/javascripts/admin/components/admin-report-storage-stats.js.es6 +++ /dev/null @@ -1,42 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { alias } from "@ember/object/computed"; -import Component from "@ember/component"; -import { setting } from "discourse/lib/computed"; - -export default Component.extend({ - classNames: ["admin-report-storage-stats"], - - backupLocation: setting("backup_location"), - backupStats: alias("model.data.backups"), - uploadStats: alias("model.data.uploads"), - - @discourseComputed("backupStats") - showBackupStats(stats) { - return stats && this.currentUser.admin; - }, - - @discourseComputed("backupLocation") - backupLocationName(backupLocation) { - return I18n.t(`admin.backups.location.${backupLocation}`); - }, - - @discourseComputed("backupStats.used_bytes") - usedBackupSpace(bytes) { - return I18n.toHumanSize(bytes); - }, - - @discourseComputed("backupStats.free_bytes") - freeBackupSpace(bytes) { - return I18n.toHumanSize(bytes); - }, - - @discourseComputed("uploadStats.used_bytes") - usedUploadSpace(bytes) { - return I18n.toHumanSize(bytes); - }, - - @discourseComputed("uploadStats.free_bytes") - freeUploadSpace(bytes) { - return I18n.toHumanSize(bytes); - } -}); 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..e64e02f3eb --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-report.js @@ -0,0 +1,413 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { makeArray } from "discourse-common/lib/helpers"; +import { alias, or, and, equal, notEmpty, not } from "@ember/object/computed"; +import EmberObject, { computed, action } from "@ember/object"; +import { next } from "@ember/runloop"; +import Component from "@ember/component"; +import ReportLoader from "discourse/lib/reports-loader"; +import { exportEntity } from "discourse/lib/export-csv"; +import { outputExportResult } from "discourse/lib/export-result"; +import Report, { SCHEMA_VERSION } from "admin/models/report"; +import { isPresent } from "@ember/utils"; +import { isTesting } 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: [ + "isVisible", + "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, + 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"), + isVisible: not("isHidden"), + + init() { + this._super(...arguments); + + this._reports = []; + }, + + isHidden: computed("siteSettings.dashboard_hidden_reports", function() { + return (this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean) + .includes(this.dataSourceName); + }), + + startDate: computed("filters.startDate", function() { + if (this.filters && isPresent(this.filters.startDate)) { + return moment(this.filters.startDate, "YYYY-MM-DD"); + } else { + return moment(); + } + }), + + endDate: computed("filters.endDate", function() { + if (this.filters && isPresent(this.filters.endDate)) { + return moment(this.filters.endDate, "YYYY-MM-DD"); + } else { + return moment(); + } + }), + + didReceiveAttrs() { + this._super(...arguments); + + 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( + "dataSourceName", + "startDate", + "endDate", + "filters.customFilters" + ) + reportKey(dataSourceName, startDate, endDate, customFilters) { + if (!dataSourceName || !startDate || !endDate) return null; + + startDate = startDate.toISOString(true).split("T")[0]; + endDate = endDate.toISOString(true).split("T")[0]; + + let reportKey = "reports:"; + reportKey += [ + dataSourceName, + isTesting() ? "start" : startDate.replace(/-/g, ""), + isTesting() ? "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; + }, + + @action + onChangeDateRange(range) { + this.send("refreshReport", { + startDate: range.from, + endDate: range.to + }); + }, + + @action + applyFilter(id, value) { + let customFilters = this.get("filters.customFilters") || {}; + + if (typeof value === "undefined") { + delete customFilters[id]; + } else { + customFilters[id] = value; + } + + this.send("refreshReport", { + filters: customFilters + }); + }, + + @action + refreshReport(options = {}) { + this.attrs.onRefresh({ + type: this.get("model.type"), + startDate: + typeof options.startDate === "undefined" + ? this.startDate + : options.startDate, + endDate: + typeof options.endDate === "undefined" ? this.endDate : options.endDate, + filters: + typeof options.filters === "undefined" + ? this.get("filters.customFilters") + : options.filters + }); + }, + + @action + exportCsv() { + const args = { + name: this.get("model.type"), + start_date: this.startDate.toISOString(true).split("T")[0], + end_date: this.endDate.toISOString(true).split("T")[0] + }; + + const customFilters = this.get("filters.customFilters"); + if (customFilters) { + Object.assign(args, customFilters); + } + + exportEntity("report", args).then(outputExportResult); + }, + + @action + 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 { + report = sort( + filteredReports.filter(r => r.report_key.includes(this.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(this.startDate) + .toISOString(true) + .split("T")[0]; + } + + if (this.endDate) { + payload.data.end_date = moment(this.endDate) + .toISOString(true) + .split("T")[0]; + } + + 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 b/app/assets/javascripts/admin/components/admin-user-field-item.js new file mode 100644 index 0000000000..d59abe4437 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-user-field-item.js @@ -0,0 +1,108 @@ +import I18n from "I18n"; +import { isEmpty } from "@ember/utils"; +import { empty } from "@ember/object/computed"; +import { scheduleOnce } from "@ember/runloop"; +import Component from "@ember/component"; +import UserField from "admin/models/user-field"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { propertyEqual } from "discourse/lib/computed"; +import { i18n } from "discourse/lib/computed"; +import discourseComputed, { + observes, + on +} from "discourse-common/utils/decorators"; + +export default Component.extend(bufferedProperty("userField"), { + editing: empty("userField.id"), + classNameBindings: [":user-field"], + + cantMoveUp: propertyEqual("userField", "firstField"), + cantMoveDown: propertyEqual("userField", "lastField"), + + userFieldsDescription: i18n("admin.user_fields.description"), + + @discourseComputed("buffered.field_type") + bufferedFieldType(fieldType) { + return UserField.fieldTypeById(fieldType); + }, + + @on("didInsertElement") + @observes("editing") + _focusOnEdit() { + if (this.editing) { + scheduleOnce("afterRender", this, "_focusName"); + } + }, + + _focusName() { + $(".user-field-name").select(); + }, + + @discourseComputed("userField.field_type") + fieldName(fieldType) { + return UserField.fieldTypeById(fieldType).get("name"); + }, + + @discourseComputed( + "userField.editable", + "userField.required", + "userField.show_on_profile", + "userField.show_on_user_card" + ) + flags(editable, required, showOnProfile, showOnUserCard) { + const ret = []; + if (editable) { + ret.push(I18n.t("admin.user_fields.editable.enabled")); + } + if (required) { + ret.push(I18n.t("admin.user_fields.required.enabled")); + } + if (showOnProfile) { + ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); + } + if (showOnUserCard) { + ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled")); + } + + return ret.join(", "); + }, + + actions: { + save() { + const buffered = this.buffered; + const attrs = buffered.getProperties( + "name", + "description", + "field_type", + "editable", + "required", + "show_on_profile", + "show_on_user_card", + "options" + ); + + this.userField + .save(attrs) + .then(() => { + this.set("editing", false); + this.commitBuffer(); + }) + .catch(popupAjaxError); + }, + + edit() { + this.set("editing", true); + }, + + cancel() { + const id = this.get("userField.id"); + if (isEmpty(id)) { + this.destroyAction(this.userField); + } else { + this.rollbackBuffer(); + this.set("editing", false); + } + } + } +}); 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.es6 deleted file mode 100644 index a02404de98..0000000000 --- a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 +++ /dev/null @@ -1,107 +0,0 @@ -import { isEmpty } from "@ember/utils"; -import { empty } from "@ember/object/computed"; -import { scheduleOnce } from "@ember/runloop"; -import Component from "@ember/component"; -import UserField from "admin/models/user-field"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { propertyEqual } from "discourse/lib/computed"; -import { i18n } from "discourse/lib/computed"; -import discourseComputed, { - observes, - on -} from "discourse-common/utils/decorators"; - -export default Component.extend(bufferedProperty("userField"), { - editing: empty("userField.id"), - classNameBindings: [":user-field"], - - cantMoveUp: propertyEqual("userField", "firstField"), - cantMoveDown: propertyEqual("userField", "lastField"), - - userFieldsDescription: i18n("admin.user_fields.description"), - - @discourseComputed("buffered.field_type") - bufferedFieldType(fieldType) { - return UserField.fieldTypeById(fieldType); - }, - - @on("didInsertElement") - @observes("editing") - _focusOnEdit() { - if (this.editing) { - scheduleOnce("afterRender", this, "_focusName"); - } - }, - - _focusName() { - $(".user-field-name").select(); - }, - - @discourseComputed("userField.field_type") - fieldName(fieldType) { - return UserField.fieldTypeById(fieldType).get("name"); - }, - - @discourseComputed( - "userField.editable", - "userField.required", - "userField.show_on_profile", - "userField.show_on_user_card" - ) - flags(editable, required, showOnProfile, showOnUserCard) { - const ret = []; - if (editable) { - ret.push(I18n.t("admin.user_fields.editable.enabled")); - } - if (required) { - ret.push(I18n.t("admin.user_fields.required.enabled")); - } - if (showOnProfile) { - ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); - } - if (showOnUserCard) { - ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled")); - } - - return ret.join(", "); - }, - - actions: { - save() { - const buffered = this.buffered; - const attrs = buffered.getProperties( - "name", - "description", - "field_type", - "editable", - "required", - "show_on_profile", - "show_on_user_card", - "options" - ); - - this.userField - .save(attrs) - .then(() => { - this.set("editing", false); - this.commitBuffer(); - }) - .catch(popupAjaxError); - }, - - edit() { - this.set("editing", true); - }, - - cancel() { - const id = this.get("userField.id"); - if (isEmpty(id)) { - this.destroyAction(this.userField); - } else { - this.rollbackBuffer(); - this.set("editing", false); - } - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-watched-word.js b/app/assets/javascripts/admin/components/admin-watched-word.js new file mode 100644 index 0000000000..28a1771246 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-watched-word.js @@ -0,0 +1,29 @@ +import I18n from "I18n"; +import Component from "@ember/component"; +import { iconHTML } from "discourse-common/lib/icon-library"; + +export default Component.extend({ + classNames: ["watched-word"], + watchedWord: null, + xIcon: iconHTML("times").htmlSafe(), + + init() { + this._super(...arguments); + this.set("watchedWord", this.get("word.word")); + }, + + click() { + this.word + .destroy() + .then(() => { + this.action(this.word); + }) + .catch(e => { + bootbox.alert( + I18n.t("generic_error_with_reason", { + error: `http: ${e.status} - ${e.body}` + }) + ); + }); + } +}); diff --git a/app/assets/javascripts/admin/components/admin-watched-word.js.es6 b/app/assets/javascripts/admin/components/admin-watched-word.js.es6 deleted file mode 100644 index d4f5108c0f..0000000000 --- a/app/assets/javascripts/admin/components/admin-watched-word.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -import Component from "@ember/component"; -import { iconHTML } from "discourse-common/lib/icon-library"; - -export default Component.extend({ - classNames: ["watched-word"], - watchedWord: null, - xIcon: iconHTML("times").htmlSafe(), - - init() { - this._super(...arguments); - this.set("watchedWord", this.get("word.word")); - }, - - click() { - this.word - .destroy() - .then(() => { - this.action(this.word); - }) - .catch(e => { - bootbox.alert( - I18n.t("generic_error_with_reason", { - error: `http: ${e.status} - ${e.body}` - }) - ); - }); - } -}); diff --git a/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js b/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js new file mode 100644 index 0000000000..7e76d8f6bd --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js @@ -0,0 +1,47 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; +import Component from "@ember/component"; + +export default Component.extend({ + classNames: ["hook-event"], + typeName: alias("type.name"), + + @discourseComputed("typeName") + name(typeName) { + return I18n.t(`admin.web_hooks.${typeName}_event.name`); + }, + + @discourseComputed("typeName") + details(typeName) { + return I18n.t(`admin.web_hooks.${typeName}_event.details`); + }, + + @discourseComputed("model.[]", "typeName") + eventTypeExists(eventTypes, typeName) { + return eventTypes.any(event => event.name === typeName); + }, + + @discourseComputed("eventTypeExists") + enabled: { + get(eventTypeExists) { + return eventTypeExists; + }, + set(value, eventTypeExists) { + const type = this.type; + const model = this.model; + // add an association when not exists + if (value !== eventTypeExists) { + if (value) { + model.addObject(type); + } else { + model.removeObjects( + model.filter(eventType => eventType.name === type.name) + ); + } + } + + return value; + } + } +}); 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.es6 deleted file mode 100644 index a38695c735..0000000000 --- a/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 +++ /dev/null @@ -1,46 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { alias } from "@ember/object/computed"; -import Component from "@ember/component"; - -export default Component.extend({ - classNames: ["hook-event"], - typeName: alias("type.name"), - - @discourseComputed("typeName") - name(typeName) { - return I18n.t(`admin.web_hooks.${typeName}_event.name`); - }, - - @discourseComputed("typeName") - details(typeName) { - return I18n.t(`admin.web_hooks.${typeName}_event.details`); - }, - - @discourseComputed("model.[]", "typeName") - eventTypeExists(eventTypes, typeName) { - return eventTypes.any(event => event.name === typeName); - }, - - @discourseComputed("eventTypeExists") - enabled: { - get(eventTypeExists) { - return eventTypeExists; - }, - set(value, eventTypeExists) { - const type = this.type; - const model = this.model; - // add an association when not exists - if (value !== eventTypeExists) { - if (value) { - model.addObject(type); - } else { - model.removeObjects( - model.filter(eventType => eventType.name === type.name) - ); - } - } - - return value; - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-web-hook-event.js b/app/assets/javascripts/admin/components/admin-web-hook-event.js new file mode 100644 index 0000000000..11bb2b4024 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-web-hook-event.js @@ -0,0 +1,110 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import Component from "@ember/component"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter"; + +export default Component.extend({ + tagName: "li", + expandDetails: null, + expandDetailsRequestKey: "request", + expandDetailsResponseKey: "response", + + @discourseComputed("model.status") + statusColorClasses(status) { + if (!status) return ""; + + if (status >= 200 && status <= 299) { + return "text-successful"; + } else { + return "text-danger"; + } + }, + + @discourseComputed("model.created_at") + createdAt(createdAt) { + return moment(createdAt).format("YYYY-MM-DD HH:mm:ss"); + }, + + @discourseComputed("model.duration") + completion(duration) { + const seconds = Math.floor(duration / 10.0) / 100.0; + return I18n.t("admin.web_hooks.events.completed_in", { count: seconds }); + }, + + @discourseComputed("expandDetails") + expandRequestIcon(expandDetails) { + return expandDetails === this.expandDetailsRequestKey + ? "ellipsis-h" + : "ellipsis-v"; + }, + + @discourseComputed("expandDetails") + expandResponseIcon(expandDetails) { + return expandDetails === this.expandDetailsResponseKey + ? "ellipsis-h" + : "ellipsis-v"; + }, + + actions: { + redeliver() { + return bootbox.confirm( + I18n.t("admin.web_hooks.events.redeliver_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + ajax( + `/admin/api/web_hooks/${this.get( + "model.web_hook_id" + )}/events/${this.get("model.id")}/redeliver`, + { type: "POST" } + ) + .then(json => { + this.set("model", json.web_hook_event); + }) + .catch(popupAjaxError); + } + } + ); + }, + + toggleRequest() { + const expandDetailsKey = this.expandDetailsRequestKey; + + if (this.expandDetails !== expandDetailsKey) { + let headers = _.extend( + { + "Request URL": this.get("model.request_url"), + "Request method": "POST" + }, + ensureJSON(this.get("model.headers")) + ); + this.setProperties({ + headers: plainJSON(headers), + body: prettyJSON(this.get("model.payload")), + expandDetails: expandDetailsKey, + bodyLabel: I18n.t("admin.web_hooks.events.payload") + }); + } else { + this.set("expandDetails", null); + } + }, + + toggleResponse() { + const expandDetailsKey = this.expandDetailsResponseKey; + + if (this.expandDetails !== expandDetailsKey) { + this.setProperties({ + headers: plainJSON(this.get("model.response_headers")), + body: this.get("model.response_body"), + expandDetails: expandDetailsKey, + bodyLabel: I18n.t("admin.web_hooks.events.body") + }); + } else { + this.set("expandDetails", null); + } + } + } +}); 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.es6 deleted file mode 100644 index 365e22aa67..0000000000 --- a/app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 +++ /dev/null @@ -1,109 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Component from "@ember/component"; -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter"; - -export default Component.extend({ - tagName: "li", - expandDetails: null, - expandDetailsRequestKey: "request", - expandDetailsResponseKey: "response", - - @discourseComputed("model.status") - statusColorClasses(status) { - if (!status) return ""; - - if (status >= 200 && status <= 299) { - return "text-successful"; - } else { - return "text-danger"; - } - }, - - @discourseComputed("model.created_at") - createdAt(createdAt) { - return moment(createdAt).format("YYYY-MM-DD HH:mm:ss"); - }, - - @discourseComputed("model.duration") - completion(duration) { - const seconds = Math.floor(duration / 10.0) / 100.0; - return I18n.t("admin.web_hooks.events.completed_in", { count: seconds }); - }, - - @discourseComputed("expandDetails") - expandRequestIcon(expandDetails) { - return expandDetails === this.expandDetailsRequestKey - ? "ellipsis-h" - : "ellipsis-v"; - }, - - @discourseComputed("expandDetails") - expandResponseIcon(expandDetails) { - return expandDetails === this.expandDetailsResponseKey - ? "ellipsis-h" - : "ellipsis-v"; - }, - - actions: { - redeliver() { - return bootbox.confirm( - I18n.t("admin.web_hooks.events.redeliver_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - ajax( - `/admin/api/web_hooks/${this.get( - "model.web_hook_id" - )}/events/${this.get("model.id")}/redeliver`, - { type: "POST" } - ) - .then(json => { - this.set("model", json.web_hook_event); - }) - .catch(popupAjaxError); - } - } - ); - }, - - toggleRequest() { - const expandDetailsKey = this.expandDetailsRequestKey; - - if (this.expandDetails !== expandDetailsKey) { - let headers = _.extend( - { - "Request URL": this.get("model.request_url"), - "Request method": "POST" - }, - ensureJSON(this.get("model.headers")) - ); - this.setProperties({ - headers: plainJSON(headers), - body: prettyJSON(this.get("model.payload")), - expandDetails: expandDetailsKey, - bodyLabel: I18n.t("admin.web_hooks.events.payload") - }); - } else { - this.set("expandDetails", null); - } - }, - - toggleResponse() { - const expandDetailsKey = this.expandDetailsResponseKey; - - if (this.expandDetails !== expandDetailsKey) { - this.setProperties({ - headers: plainJSON(this.get("model.response_headers")), - body: this.get("model.response_body"), - expandDetails: expandDetailsKey, - bodyLabel: I18n.t("admin.web_hooks.events.body") - }); - } else { - this.set("expandDetails", null); - } - } - } -}); diff --git a/app/assets/javascripts/admin/components/admin-web-hook-status.js b/app/assets/javascripts/admin/components/admin-web-hook-status.js new file mode 100644 index 0000000000..6c1591bc4c --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-web-hook-status.js @@ -0,0 +1,38 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import Component from "@ember/component"; +import { iconHTML } from "discourse-common/lib/icon-library"; + +export default Component.extend({ + classes: ["text-muted", "text-danger", "text-successful", "text-muted"], + icons: ["far-circle", "times-circle", "circle", "circle"], + circleIcon: null, + deliveryStatus: null, + + @discourseComputed("deliveryStatuses", "model.last_delivery_status") + status(deliveryStatuses, lastDeliveryStatus) { + return deliveryStatuses.find(s => s.id === lastDeliveryStatus); + }, + + @discourseComputed("status.id", "icons") + icon(statusId, icons) { + return icons[statusId - 1]; + }, + + @discourseComputed("status.id", "classes") + class(statusId, classes) { + return classes[statusId - 1]; + }, + + didReceiveAttrs() { + this._super(...arguments); + this.set( + "circleIcon", + iconHTML(this.icon, { class: this.class }).htmlSafe() + ); + this.set( + "deliveryStatus", + I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`) + ); + } +}); 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.es6 deleted file mode 100644 index 0c24edc9d6..0000000000 --- a/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Component from "@ember/component"; -import { iconHTML } from "discourse-common/lib/icon-library"; - -export default Component.extend({ - classes: ["text-muted", "text-danger", "text-successful", "text-muted"], - icons: ["far-circle", "times-circle", "circle", "circle"], - circleIcon: null, - deliveryStatus: null, - - @discourseComputed("deliveryStatuses", "model.last_delivery_status") - status(deliveryStatuses, lastDeliveryStatus) { - return deliveryStatuses.find(s => s.id === lastDeliveryStatus); - }, - - @discourseComputed("status.id", "icons") - icon(statusId, icons) { - return icons[statusId - 1]; - }, - - @discourseComputed("status.id", "classes") - class(statusId, classes) { - return classes[statusId - 1]; - }, - - didReceiveAttrs() { - this._super(...arguments); - this.set( - "circleIcon", - iconHTML(this.icon, { class: this.class }).htmlSafe() - ); - this.set( - "deliveryStatus", - I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`) - ); - } -}); 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 b/app/assets/javascripts/admin/components/email-styles-editor.js new file mode 100644 index 0000000000..93d4df6b7d --- /dev/null +++ b/app/assets/javascripts/admin/components/email-styles-editor.js @@ -0,0 +1,53 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { reads } from "@ember/object/computed"; +import Component from "@ember/component"; + +export default Component.extend({ + editorId: reads("fieldName"), + + @discourseComputed("fieldName") + currentEditorMode(fieldName) { + return fieldName === "css" ? "scss" : fieldName; + }, + + @discourseComputed("fieldName", "styles.html", "styles.css") + resetDisabled(fieldName) { + return ( + this.get(`styles.${fieldName}`) === + this.get(`styles.default_${fieldName}`) + ); + }, + + @discourseComputed("styles", "fieldName") + editorContents: { + get(styles, fieldName) { + return styles[fieldName]; + }, + set(value, styles, fieldName) { + styles.setField(fieldName, value); + return value; + } + }, + + actions: { + reset() { + bootbox.confirm( + I18n.t("admin.customize.email_style.reset_confirm", { + fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`) + }), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + this.styles.setField( + this.fieldName, + this.styles.get(`default_${this.fieldName}`) + ); + this.notifyPropertyChange("editorContents"); + } + } + ); + } + } +}); diff --git a/app/assets/javascripts/admin/components/email-styles-editor.js.es6 b/app/assets/javascripts/admin/components/email-styles-editor.js.es6 deleted file mode 100644 index ef5cdb077e..0000000000 --- a/app/assets/javascripts/admin/components/email-styles-editor.js.es6 +++ /dev/null @@ -1,52 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { reads } from "@ember/object/computed"; -import Component from "@ember/component"; - -export default Component.extend({ - editorId: reads("fieldName"), - - @discourseComputed("fieldName") - currentEditorMode(fieldName) { - return fieldName === "css" ? "scss" : fieldName; - }, - - @discourseComputed("fieldName", "styles.html", "styles.css") - resetDisabled(fieldName) { - return ( - this.get(`styles.${fieldName}`) === - this.get(`styles.default_${fieldName}`) - ); - }, - - @discourseComputed("styles", "fieldName") - editorContents: { - get(styles, fieldName) { - return styles[fieldName]; - }, - set(value, styles, fieldName) { - styles.setField(fieldName, value); - return value; - } - }, - - actions: { - reset() { - bootbox.confirm( - I18n.t("admin.customize.email_style.reset_confirm", { - fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`) - }), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - this.styles.setField( - this.fieldName, - this.styles.get(`default_${this.fieldName}`) - ); - this.notifyPropertyChange("editorContents"); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/components/embeddable-host.js b/app/assets/javascripts/admin/components/embeddable-host.js new file mode 100644 index 0000000000..cc96741fed --- /dev/null +++ b/app/assets/javascripts/admin/components/embeddable-host.js @@ -0,0 +1,81 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { isEmpty } from "@ember/utils"; +import { or } from "@ember/object/computed"; +import { schedule } from "@ember/runloop"; +import Component from "@ember/component"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; +import { on, observes } from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import Category from "discourse/models/category"; + +export default Component.extend(bufferedProperty("host"), { + editToggled: false, + tagName: "tr", + categoryId: null, + + editing: or("host.isNew", "editToggled"), + + @on("didInsertElement") + @observes("editing") + _focusOnInput() { + schedule("afterRender", () => { + this.element.querySelector(".host-name").focus(); + }); + }, + + @discourseComputed("buffered.host", "host.isSaving") + cantSave(host, isSaving) { + return isSaving || isEmpty(host); + }, + + actions: { + edit() { + this.set("categoryId", this.get("host.category.id")); + this.set("editToggled", true); + }, + + save() { + if (this.cantSave) { + return; + } + + const props = this.buffered.getProperties( + "host", + "path_whitelist", + "class_name" + ); + props.category_id = this.categoryId; + + const host = this.host; + + host + .save(props) + .then(() => { + host.set("category", Category.findById(this.categoryId)); + this.set("editToggled", false); + }) + .catch(popupAjaxError); + }, + + delete() { + bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), result => { + if (result) { + this.host.destroyRecord().then(() => { + this.deleteHost(this.host); + }); + } + }); + }, + + cancel() { + const host = this.host; + if (host.get("isNew")) { + this.deleteHost(host); + } else { + this.rollbackBuffer(); + this.set("editToggled", false); + } + } + } +}); diff --git a/app/assets/javascripts/admin/components/embeddable-host.js.es6 b/app/assets/javascripts/admin/components/embeddable-host.js.es6 deleted file mode 100644 index 1d853b8986..0000000000 --- a/app/assets/javascripts/admin/components/embeddable-host.js.es6 +++ /dev/null @@ -1,80 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { isEmpty } from "@ember/utils"; -import { or } from "@ember/object/computed"; -import { schedule } from "@ember/runloop"; -import Component from "@ember/component"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; -import { on, observes } from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import Category from "discourse/models/category"; - -export default Component.extend(bufferedProperty("host"), { - editToggled: false, - tagName: "tr", - categoryId: null, - - editing: or("host.isNew", "editToggled"), - - @on("didInsertElement") - @observes("editing") - _focusOnInput() { - schedule("afterRender", () => { - this.element.querySelector(".host-name").focus(); - }); - }, - - @discourseComputed("buffered.host", "host.isSaving") - cantSave(host, isSaving) { - return isSaving || isEmpty(host); - }, - - actions: { - edit() { - this.set("categoryId", this.get("host.category.id")); - this.set("editToggled", true); - }, - - save() { - if (this.cantSave) { - return; - } - - const props = this.buffered.getProperties( - "host", - "path_whitelist", - "class_name" - ); - props.category_id = this.categoryId; - - const host = this.host; - - host - .save(props) - .then(() => { - host.set("category", Category.findById(this.categoryId)); - this.set("editToggled", false); - }) - .catch(popupAjaxError); - }, - - delete() { - bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), result => { - if (result) { - this.host.destroyRecord().then(() => { - this.deleteHost(this.host); - }); - } - }); - }, - - cancel() { - const host = this.host; - if (host.get("isNew")) { - this.deleteHost(host); - } else { - this.rollbackBuffer(); - this.set("editToggled", false); - } - } - } -}); 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 b/app/assets/javascripts/admin/components/inline-edit-checkbox.js new file mode 100644 index 0000000000..2d681202b9 --- /dev/null +++ b/app/assets/javascripts/admin/components/inline-edit-checkbox.js @@ -0,0 +1,42 @@ +import I18n from "I18n"; +import Component from "@ember/component"; +import discourseComputed, { observes } from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNames: ["inline-edit"], + + checked: null, + checkedInternal: null, + + init() { + this._super(...arguments); + + this.set("checkedInternal", this.checked); + }, + + @observes("checked") + checkedChanged() { + this.set("checkedInternal", this.checked); + }, + + @discourseComputed("labelKey") + label(key) { + return I18n.t(key); + }, + + @discourseComputed("checked", "checkedInternal") + changed(checked, checkedInternal) { + return !!checked !== !!checkedInternal; + }, + + actions: { + cancelled() { + this.set("checkedInternal", this.checked); + }, + + finished() { + this.set("checked", this.checkedInternal); + this.action(); + } + } +}); diff --git a/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 b/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 deleted file mode 100644 index 2eb2c3ee5a..0000000000 --- a/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 +++ /dev/null @@ -1,41 +0,0 @@ -import Component from "@ember/component"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNames: ["inline-edit"], - - checked: null, - checkedInternal: null, - - init() { - this._super(...arguments); - - this.set("checkedInternal", this.checked); - }, - - @observes("checked") - checkedChanged() { - this.set("checkedInternal", this.checked); - }, - - @discourseComputed("labelKey") - label(key) { - return I18n.t(key); - }, - - @discourseComputed("checked", "checkedInternal") - changed(checked, checkedInternal) { - return !!checked !== !!checkedInternal; - }, - - actions: { - cancelled() { - this.set("checkedInternal", this.checked); - }, - - finished() { - this.set("checked", this.checkedInternal); - this.action(); - } - } -}); 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 b/app/assets/javascripts/admin/components/ip-lookup.js new file mode 100644 index 0000000000..d004555513 --- /dev/null +++ b/app/assets/javascripts/admin/components/ip-lookup.js @@ -0,0 +1,114 @@ +import I18n from "I18n"; +import EmberObject from "@ember/object"; +import { later } from "@ember/runloop"; +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; +import { ajax } from "discourse/lib/ajax"; +import AdminUser from "admin/models/admin-user"; +import copyText from "discourse/lib/copy-text"; + +export default Component.extend({ + classNames: ["ip-lookup"], + + @discourseComputed("other_accounts.length", "totalOthersWithSameIP") + otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) { + // can only delete up to 50 accounts at a time + const total = Math.min(50, totalOthersWithSameIP || 0); + const visible = Math.min(50, otherAccountsLength || 0); + return Math.max(visible, total); + }, + + actions: { + lookup() { + this.set("show", true); + + if (!this.location) { + ajax("/admin/users/ip-info", { data: { ip: this.ip } }).then(location => + this.set("location", EmberObject.create(location)) + ); + } + + if (!this.other_accounts) { + this.set("otherAccountsLoading", true); + + const data = { + ip: this.ip, + exclude: this.userId, + order: "trust_level DESC" + }; + + ajax("/admin/users/total-others-with-same-ip", { data }).then(result => + this.set("totalOthersWithSameIP", result.total) + ); + + AdminUser.findAll("active", data).then(users => { + this.setProperties({ + other_accounts: users, + otherAccountsLoading: false + }); + }); + } + }, + + hide() { + this.set("show", false); + }, + + copy() { + let text = `IP: ${this.ip}\n`; + const location = this.location; + if (location) { + if (location.hostname) { + text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`; + } + + text += I18n.t("ip_lookup.location"); + if (location.location) { + text += `: ${location.location}\n`; + } else { + text += `: ${I18n.t("ip_lookup.location_not_found")}\n`; + } + + if (location.organization) { + text += I18n.t("ip_lookup.organisation"); + text += `: ${location.organization}\n`; + } + } + + const $copyRange = $('

'); + $copyRange.html(text.trim().replace(/\n/g, "
")); + $(document.body).append($copyRange); + if (copyText(text, $copyRange[0])) { + this.set("copied", true); + later(() => this.set("copied", false), 2000); + } + $copyRange.remove(); + }, + + deleteOtherAccounts() { + bootbox.confirm( + I18n.t("ip_lookup.confirm_delete_other_accounts"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + this.setProperties({ + other_accounts: null, + otherAccountsLoading: true, + totalOthersWithSameIP: null + }); + + ajax("/admin/users/delete-others-with-same-ip.json", { + type: "DELETE", + data: { + ip: this.ip, + exclude: this.userId, + order: "trust_level DESC" + } + }).then(() => this.send("lookup")); + } + } + ); + } + } +}); diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js.es6 deleted file mode 100644 index b27191d617..0000000000 --- a/app/assets/javascripts/admin/components/ip-lookup.js.es6 +++ /dev/null @@ -1,113 +0,0 @@ -import EmberObject from "@ember/object"; -import { later } from "@ember/runloop"; -import Component from "@ember/component"; -import discourseComputed from "discourse-common/utils/decorators"; -import { ajax } from "discourse/lib/ajax"; -import AdminUser from "admin/models/admin-user"; -import copyText from "discourse/lib/copy-text"; - -export default Component.extend({ - classNames: ["ip-lookup"], - - @discourseComputed("other_accounts.length", "totalOthersWithSameIP") - otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) { - // can only delete up to 50 accounts at a time - const total = Math.min(50, totalOthersWithSameIP || 0); - const visible = Math.min(50, otherAccountsLength || 0); - return Math.max(visible, total); - }, - - actions: { - lookup() { - this.set("show", true); - - if (!this.location) { - ajax("/admin/users/ip-info", { data: { ip: this.ip } }).then(location => - this.set("location", EmberObject.create(location)) - ); - } - - if (!this.other_accounts) { - this.set("otherAccountsLoading", true); - - const data = { - ip: this.ip, - exclude: this.userId, - order: "trust_level DESC" - }; - - ajax("/admin/users/total-others-with-same-ip", { data }).then(result => - this.set("totalOthersWithSameIP", result.total) - ); - - AdminUser.findAll("active", data).then(users => { - this.setProperties({ - other_accounts: users, - otherAccountsLoading: false - }); - }); - } - }, - - hide() { - this.set("show", false); - }, - - copy() { - let text = `IP: ${this.ip}\n`; - const location = this.location; - if (location) { - if (location.hostname) { - text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`; - } - - text += I18n.t("ip_lookup.location"); - if (location.location) { - text += `: ${location.location}\n`; - } else { - text += `: ${I18n.t("ip_lookup.location_not_found")}\n`; - } - - if (location.organization) { - text += I18n.t("ip_lookup.organisation"); - text += `: ${location.organization}\n`; - } - } - - const $copyRange = $('

'); - $copyRange.html(text.trim().replace(/\n/g, "
")); - $(document.body).append($copyRange); - if (copyText(text, $copyRange[0])) { - this.set("copied", true); - later(() => this.set("copied", false), 2000); - } - $copyRange.remove(); - }, - - deleteOtherAccounts() { - bootbox.confirm( - I18n.t("ip_lookup.confirm_delete_other_accounts"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - this.setProperties({ - other_accounts: null, - otherAccountsLoading: true, - totalOthersWithSameIP: null - }); - - ajax("/admin/users/delete-others-with-same-ip.json", { - type: "DELETE", - data: { - ip: this.ip, - exclude: this.userId, - order: "trust_level DESC" - } - }).then(() => this.send("lookup")); - } - } - ); - } - } -}); 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 b/app/assets/javascripts/admin/components/penalty-post-action.js new file mode 100644 index 0000000000..aeea3406c8 --- /dev/null +++ b/app/assets/javascripts/admin/components/penalty-post-action.js @@ -0,0 +1,41 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { equal } from "@ember/object/computed"; +import Component from "@ember/component"; +import { afterRender } from "discourse-common/utils/decorators"; + +const ACTIONS = ["delete", "delete_replies", "edit", "none"]; + +export default Component.extend({ + postId: null, + postAction: null, + postEdit: null, + + @discourseComputed + penaltyActions() { + return ACTIONS.map(id => { + return { id, name: I18n.t(`admin.user.penalty_post_${id}`) }; + }); + }, + + editing: equal("postAction", "edit"), + + actions: { + penaltyChanged(postAction) { + this.set("postAction", postAction); + + // If we switch to edit mode, jump to the edit textarea + if (postAction === "edit") { + this._focusEditTextarea(); + } + } + }, + + @afterRender + _focusEditTextarea() { + const elem = this.element; + const body = elem.closest(".modal-body"); + body.scrollTo(0, body.clientHeight); + elem.querySelector(".post-editor").focus(); + } +}); diff --git a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 b/app/assets/javascripts/admin/components/penalty-post-action.js.es6 deleted file mode 100644 index 916546452c..0000000000 --- a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { equal } from "@ember/object/computed"; -import Component from "@ember/component"; -import { afterRender } from "discourse-common/utils/decorators"; - -const ACTIONS = ["delete", "delete_replies", "edit", "none"]; - -export default Component.extend({ - postId: null, - postAction: null, - postEdit: null, - - @discourseComputed - penaltyActions() { - return ACTIONS.map(id => { - return { id, name: I18n.t(`admin.user.penalty_post_${id}`) }; - }); - }, - - editing: equal("postAction", "edit"), - - actions: { - penaltyChanged(postAction) { - this.set("postAction", postAction); - - // If we switch to edit mode, jump to the edit textarea - if (postAction === "edit") { - this._focusEditTextarea(); - } - } - }, - - @afterRender - _focusEditTextarea() { - const elem = this.element; - const body = elem.closest(".modal-body"); - body.scrollTo(0, body.clientHeight); - elem.querySelector(".post-editor").focus(); - } -}); diff --git a/app/assets/javascripts/admin/components/permalink-form.js b/app/assets/javascripts/admin/components/permalink-form.js new file mode 100644 index 0000000000..81e9346d6f --- /dev/null +++ b/app/assets/javascripts/admin/components/permalink-form.js @@ -0,0 +1,88 @@ +import I18n from "I18n"; +import { schedule } from "@ember/runloop"; +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; +import { fmt } from "discourse/lib/computed"; +import Permalink from "admin/models/permalink"; + +export default Component.extend({ + classNames: ["permalink-form"], + formSubmitted: false, + permalinkType: "topic_id", + permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"), + + @discourseComputed + permalinkTypes() { + return [ + { id: "topic_id", name: I18n.t("admin.permalink.topic_id") }, + { id: "post_id", name: I18n.t("admin.permalink.post_id") }, + { id: "category_id", name: I18n.t("admin.permalink.category_id") }, + { id: "tag_name", name: I18n.t("admin.permalink.tag_name") }, + { id: "external_url", name: I18n.t("admin.permalink.external_url") } + ]; + }, + + didInsertElement() { + this._super(...arguments); + + schedule("afterRender", () => { + $(this.element.querySelector(".external-url")).keydown(e => { + // enter key + if (e.keyCode === 13) { + this.send("submit"); + } + }); + }); + }, + + focusPermalink() { + schedule("afterRender", () => + this.element.querySelector(".permalink-url").focus() + ); + }, + + actions: { + submit() { + if (!this.formSubmitted) { + this.set("formSubmitted", true); + + Permalink.create({ + url: this.url, + permalink_type: this.permalinkType, + permalink_type_value: this.permalink_type_value + }) + .save() + .then( + result => { + this.setProperties({ + url: "", + permalink_type_value: "", + formSubmitted: false + }); + + this.action(Permalink.create(result.permalink)); + + this.focusPermalink(); + }, + e => { + this.set("formSubmitted", false); + + let error; + if (e.responseJSON && e.responseJSON.errors) { + error = I18n.t("generic_error_with_reason", { + error: e.responseJSON.errors.join(". ") + }); + } else { + error = I18n.t("generic_error"); + } + bootbox.alert(error, () => this.focusPermalink()); + } + ); + } + }, + + onChangePermalinkType(type) { + this.set("permalinkType", type); + } + } +}); diff --git a/app/assets/javascripts/admin/components/permalink-form.js.es6 b/app/assets/javascripts/admin/components/permalink-form.js.es6 deleted file mode 100644 index e1b62a11df..0000000000 --- a/app/assets/javascripts/admin/components/permalink-form.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -import { schedule } from "@ember/runloop"; -import Component from "@ember/component"; -import discourseComputed from "discourse-common/utils/decorators"; -import { fmt } from "discourse/lib/computed"; -import Permalink from "admin/models/permalink"; - -export default Component.extend({ - classNames: ["permalink-form"], - formSubmitted: false, - permalinkType: "topic_id", - permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"), - - @discourseComputed - permalinkTypes() { - return [ - { id: "topic_id", name: I18n.t("admin.permalink.topic_id") }, - { id: "post_id", name: I18n.t("admin.permalink.post_id") }, - { id: "category_id", name: I18n.t("admin.permalink.category_id") }, - { id: "external_url", name: I18n.t("admin.permalink.external_url") } - ]; - }, - - didInsertElement() { - this._super(...arguments); - - schedule("afterRender", () => { - $(this.element.querySelector(".external-url")).keydown(e => { - // enter key - if (e.keyCode === 13) { - this.send("submit"); - } - }); - }); - }, - - focusPermalink() { - schedule("afterRender", () => - this.element.querySelector(".permalink-url").focus() - ); - }, - - actions: { - submit() { - if (!this.formSubmitted) { - this.set("formSubmitted", true); - - Permalink.create({ - url: this.url, - permalink_type: this.permalinkType, - permalink_type_value: this.permalink_type_value - }) - .save() - .then( - result => { - this.setProperties({ - url: "", - permalink_type_value: "", - formSubmitted: false - }); - - this.action(Permalink.create(result.permalink)); - - this.focusPermalink(); - }, - e => { - this.set("formSubmitted", false); - - let error; - if (e.responseJSON && e.responseJSON.errors) { - error = I18n.t("generic_error_with_reason", { - error: e.responseJSON.errors.join(". ") - }); - } else { - error = I18n.t("generic_error"); - } - bootbox.alert(error, () => this.focusPermalink()); - } - ); - } - }, - - onChangePermalinkType(type) { - this.set("permalinkType", type); - } - } -}); 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..97b6f19875 --- /dev/null +++ b/app/assets/javascripts/admin/components/resumable-upload.js @@ -0,0 +1,140 @@ +import getURL from "discourse-common/lib/get-url"; +import I18n from "I18n"; +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: 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 b/app/assets/javascripts/admin/components/screened-ip-address-form.js new file mode 100644 index 0000000000..1046f4c210 --- /dev/null +++ b/app/assets/javascripts/admin/components/screened-ip-address-form.js @@ -0,0 +1,97 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { schedule } from "@ember/runloop"; +import Component from "@ember/component"; +/** + A form to create an IP address that will be blocked or whitelisted. + Example usage: + + {{screened-ip-address-form action=(action "recordAdded")}} + + where action is a callback on the controller or route that will get called after + the new record is successfully saved. It is called with the new ScreenedIpAddress record + as an argument. +**/ + +import ScreenedIpAddress from "admin/models/screened-ip-address"; +import { on } from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNames: ["screened-ip-address-form"], + formSubmitted: false, + actionName: "block", + + @discourseComputed + adminWhitelistEnabled() { + return Discourse.SiteSettings.use_admin_ip_whitelist; + }, + + @discourseComputed("adminWhitelistEnabled") + actionNames(adminWhitelistEnabled) { + if (adminWhitelistEnabled) { + return [ + { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, + { + id: "do_nothing", + name: I18n.t("admin.logs.screened_ips.actions.do_nothing") + }, + { + id: "allow_admin", + name: I18n.t("admin.logs.screened_ips.actions.allow_admin") + } + ]; + } else { + return [ + { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, + { + id: "do_nothing", + name: I18n.t("admin.logs.screened_ips.actions.do_nothing") + } + ]; + } + }, + + actions: { + submit() { + if (!this.formSubmitted) { + this.set("formSubmitted", true); + const screenedIpAddress = ScreenedIpAddress.create({ + ip_address: this.ip_address, + action_name: this.actionName + }); + screenedIpAddress + .save() + .then(result => { + this.setProperties({ ip_address: "", formSubmitted: false }); + this.action(ScreenedIpAddress.create(result.screened_ip_address)); + schedule("afterRender", () => + this.element.querySelector(".ip-address-input").focus() + ); + }) + .catch(e => { + this.set("formSubmitted", false); + const msg = + e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors + ? I18n.t("generic_error_with_reason", { + error: e.jqXHR.responseJSON.errors.join(". ") + }) + : I18n.t("generic_error"); + bootbox.alert(msg, () => + this.element.querySelector(".ip-address-input").focus() + ); + }); + } + } + }, + + @on("didInsertElement") + _init() { + schedule("afterRender", () => { + $(this.element.querySelector(".ip-address-input")).keydown(e => { + if (e.keyCode === 13) { + this.send("submit"); + } + }); + }); + } +}); 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.es6 deleted file mode 100644 index 48b92641b4..0000000000 --- a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { schedule } from "@ember/runloop"; -import Component from "@ember/component"; -/** - A form to create an IP address that will be blocked or whitelisted. - Example usage: - - {{screened-ip-address-form action=(action "recordAdded")}} - - where action is a callback on the controller or route that will get called after - the new record is successfully saved. It is called with the new ScreenedIpAddress record - as an argument. -**/ - -import ScreenedIpAddress from "admin/models/screened-ip-address"; -import { on } from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNames: ["screened-ip-address-form"], - formSubmitted: false, - actionName: "block", - - @discourseComputed - adminWhitelistEnabled() { - return Discourse.SiteSettings.use_admin_ip_whitelist; - }, - - @discourseComputed("adminWhitelistEnabled") - actionNames(adminWhitelistEnabled) { - if (adminWhitelistEnabled) { - return [ - { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, - { - id: "do_nothing", - name: I18n.t("admin.logs.screened_ips.actions.do_nothing") - }, - { - id: "allow_admin", - name: I18n.t("admin.logs.screened_ips.actions.allow_admin") - } - ]; - } else { - return [ - { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, - { - id: "do_nothing", - name: I18n.t("admin.logs.screened_ips.actions.do_nothing") - } - ]; - } - }, - - actions: { - submit() { - if (!this.formSubmitted) { - this.set("formSubmitted", true); - const screenedIpAddress = ScreenedIpAddress.create({ - ip_address: this.ip_address, - action_name: this.actionName - }); - screenedIpAddress - .save() - .then(result => { - this.setProperties({ ip_address: "", formSubmitted: false }); - this.action(ScreenedIpAddress.create(result.screened_ip_address)); - schedule("afterRender", () => - this.element.querySelector(".ip-address-input").focus() - ); - }) - .catch(e => { - this.set("formSubmitted", false); - const msg = - e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors - ? I18n.t("generic_error_with_reason", { - error: e.jqXHR.responseJSON.errors.join(". ") - }) - : I18n.t("generic_error"); - bootbox.alert(msg, () => - this.element.querySelector(".ip-address-input").focus() - ); - }); - } - } - }, - - @on("didInsertElement") - _init() { - schedule("afterRender", () => { - $(this.element.querySelector(".ip-address-input")).keydown(e => { - if (e.keyCode === 13) { - this.send("submit"); - } - }); - }); - } -}); diff --git a/app/assets/javascripts/admin/components/secret-value-list.js b/app/assets/javascripts/admin/components/secret-value-list.js new file mode 100644 index 0000000000..399ec5c6e7 --- /dev/null +++ b/app/assets/javascripts/admin/components/secret-value-list.js @@ -0,0 +1,105 @@ +import I18n from "I18n"; +import { isEmpty } from "@ember/utils"; +import Component from "@ember/component"; +import { on } from "discourse-common/utils/decorators"; +import { set } from "@ember/object"; + +export default Component.extend({ + classNameBindings: [":value-list", ":secret-value-list"], + inputDelimiter: null, + collection: null, + values: null, + validationMessage: null, + + @on("didReceiveAttrs") + _setupCollection() { + const values = this.values; + + this.set( + "collection", + this._splitValues(values, this.inputDelimiter || "\n") + ); + }, + + actions: { + changeKey(index, newValue) { + if (this._checkInvalidInput(newValue)) return; + this._replaceValue(index, newValue, "key"); + }, + + changeSecret(index, newValue) { + if (this._checkInvalidInput(newValue)) return; + this._replaceValue(index, newValue, "secret"); + }, + + addValue() { + if (this._checkInvalidInput([this.newKey, this.newSecret])) return; + this._addValue(this.newKey, this.newSecret); + this.setProperties({ newKey: "", newSecret: "" }); + }, + + removeValue(value) { + this._removeValue(value); + } + }, + + _checkInvalidInput(inputs) { + this.set("validationMessage", null); + for (let input of inputs) { + if (isEmpty(input) || input.includes("|")) { + this.set( + "validationMessage", + I18n.t("admin.site_settings.secret_list.invalid_input") + ); + return true; + } + } + }, + + _addValue(value, secret) { + this.collection.addObject({ key: value, secret: secret }); + this._saveValues(); + }, + + _removeValue(value) { + const collection = this.collection; + collection.removeObject(value); + this._saveValues(); + }, + + _replaceValue(index, newValue, keyName) { + let item = this.collection[index]; + set(item, keyName, newValue); + + this._saveValues(); + }, + + _saveValues() { + this.set( + "values", + this.collection + .map(function(elem) { + return `${elem.key}|${elem.secret}`; + }) + .join("\n") + ); + }, + + _splitValues(values, delimiter) { + if (values && values.length) { + const keys = ["key", "secret"]; + var res = []; + values.split(delimiter).forEach(function(str) { + var object = {}; + str.split("|").forEach(function(a, i) { + object[keys[i]] = a; + }); + res.push(object); + }); + + return res; + } else { + return []; + } + } +}); diff --git a/app/assets/javascripts/admin/components/secret-value-list.js.es6 b/app/assets/javascripts/admin/components/secret-value-list.js.es6 deleted file mode 100644 index ea4ecf792c..0000000000 --- a/app/assets/javascripts/admin/components/secret-value-list.js.es6 +++ /dev/null @@ -1,104 +0,0 @@ -import { isEmpty } from "@ember/utils"; -import Component from "@ember/component"; -import { on } from "discourse-common/utils/decorators"; -import { set } from "@ember/object"; - -export default Component.extend({ - classNameBindings: [":value-list", ":secret-value-list"], - inputDelimiter: null, - collection: null, - values: null, - validationMessage: null, - - @on("didReceiveAttrs") - _setupCollection() { - const values = this.values; - - this.set( - "collection", - this._splitValues(values, this.inputDelimiter || "\n") - ); - }, - - actions: { - changeKey(index, newValue) { - if (this._checkInvalidInput(newValue)) return; - this._replaceValue(index, newValue, "key"); - }, - - changeSecret(index, newValue) { - if (this._checkInvalidInput(newValue)) return; - this._replaceValue(index, newValue, "secret"); - }, - - addValue() { - if (this._checkInvalidInput([this.newKey, this.newSecret])) return; - this._addValue(this.newKey, this.newSecret); - this.setProperties({ newKey: "", newSecret: "" }); - }, - - removeValue(value) { - this._removeValue(value); - } - }, - - _checkInvalidInput(inputs) { - this.set("validationMessage", null); - for (let input of inputs) { - if (isEmpty(input) || input.includes("|")) { - this.set( - "validationMessage", - I18n.t("admin.site_settings.secret_list.invalid_input") - ); - return true; - } - } - }, - - _addValue(value, secret) { - this.collection.addObject({ key: value, secret: secret }); - this._saveValues(); - }, - - _removeValue(value) { - const collection = this.collection; - collection.removeObject(value); - this._saveValues(); - }, - - _replaceValue(index, newValue, keyName) { - let item = this.collection[index]; - set(item, keyName, newValue); - - this._saveValues(); - }, - - _saveValues() { - this.set( - "values", - this.collection - .map(function(elem) { - return `${elem.key}|${elem.secret}`; - }) - .join("\n") - ); - }, - - _splitValues(values, delimiter) { - if (values && values.length) { - const keys = ["key", "secret"]; - var res = []; - values.split(delimiter).forEach(function(str) { - var object = {}; - str.split("|").forEach(function(a, i) { - object[keys[i]] = a; - }); - res.push(object); - }); - - return res; - } else { - return []; - } - } -}); 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/simple-list.js b/app/assets/javascripts/admin/components/simple-list.js new file mode 100644 index 0000000000..3f43885d77 --- /dev/null +++ b/app/assets/javascripts/admin/components/simple-list.js @@ -0,0 +1,57 @@ +import { empty } from "@ember/object/computed"; +import Component from "@ember/component"; +import { action } from "@ember/object"; +import { on } from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNameBindings: [":simple-list", ":value-list"], + inputEmpty: empty("newValue"), + inputDelimiter: null, + newValue: "", + collection: null, + values: null, + + @on("didReceiveAttrs") + _setupCollection() { + this.set("collection", this._splitValues(this.values, this.inputDelimiter)); + }, + + keyDown(event) { + if (event.which === 13) { + this.addValue(this.newValue); + return; + } + }, + + @action + changeValue(index, newValue) { + this.collection.replace(index, 1, [newValue]); + this.collection.arrayContentDidChange(index); + this._onChange(); + }, + + @action + addValue(newValue) { + if (this.inputEmpty) return; + + this.set("newValue", null); + this.collection.addObject(newValue); + this._onChange(); + }, + + @action + removeValue(value) { + this.collection.removeObject(value); + this._onChange(); + }, + + _onChange() { + this.attrs.onChange && this.attrs.onChange(this.collection); + }, + + _splitValues(values, delimiter) { + return values && values.length + ? values.split(delimiter || "\n").filter(Boolean) + : []; + } +}); 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/simple-list.js b/app/assets/javascripts/admin/components/site-settings/simple-list.js new file mode 100644 index 0000000000..aab078bbe3 --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/simple-list.js @@ -0,0 +1,11 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; + +export default Component.extend({ + inputDelimiter: "|", + + @action + onChange(value) { + this.set("value", value.join(this.inputDelimiter || "\n")); + } +}); diff --git a/app/assets/javascripts/admin/components/site-settings/tag-list.js b/app/assets/javascripts/admin/components/site-settings/tag-list.js new file mode 100644 index 0000000000..011c734b7d --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/tag-list.js @@ -0,0 +1,17 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import Component from "@ember/component"; +import { action } from "@ember/object"; + +export default Component.extend({ + @discourseComputed("value") + selectedTags: { + get(value) { + return value.split("|").filter(Boolean); + } + }, + + @action + changeSelectedTags(tags) { + this.set("value", tags.join("|")); + } +}); 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.es6 deleted file mode 100644 index c8a8e0a06f..0000000000 --- a/app/assets/javascripts/admin/components/site-settings/tag-list.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Component from "@ember/component"; - -export default Component.extend({ - @discourseComputed("value") - selectedTags: { - get(value) { - return value.split("|"); - }, - set(value) { - this.set("value", value.join("|")); - return value; - } - } -}); 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 b/app/assets/javascripts/admin/components/tags-uploader.js new file mode 100644 index 0000000000..7530f29e21 --- /dev/null +++ b/app/assets/javascripts/admin/components/tags-uploader.js @@ -0,0 +1,22 @@ +import I18n from "I18n"; +import { alias } from "@ember/object/computed"; +import Component from "@ember/component"; +import UploadMixin from "discourse/mixins/upload"; + +export default Component.extend(UploadMixin, { + type: "csv", + uploadUrl: "/tags/upload", + addDisabled: alias("uploading"), + elementId: "tag-uploader", + + validateUploadedFilesOptions() { + return { csvOnly: true }; + }, + + uploadDone() { + bootbox.alert(I18n.t("tagging.upload_successful"), () => { + this.refresh(); + this.closeModal(); + }); + } +}); diff --git a/app/assets/javascripts/admin/components/tags-uploader.js.es6 b/app/assets/javascripts/admin/components/tags-uploader.js.es6 deleted file mode 100644 index 88f4afc8d9..0000000000 --- a/app/assets/javascripts/admin/components/tags-uploader.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -import { alias } from "@ember/object/computed"; -import Component from "@ember/component"; -import UploadMixin from "discourse/mixins/upload"; - -export default Component.extend(UploadMixin, { - type: "csv", - uploadUrl: "/tags/upload", - addDisabled: alias("uploading"), - elementId: "tag-uploader", - - validateUploadedFilesOptions() { - return { csvOnly: true }; - }, - - uploadDone() { - bootbox.alert(I18n.t("tagging.upload_successful"), () => { - this.refresh(); - this.closeModal(); - }); - } -}); 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 b/app/assets/javascripts/admin/components/themes-list-item.js new file mode 100644 index 0000000000..56db74e7e8 --- /dev/null +++ b/app/assets/javascripts/admin/components/themes-list-item.js @@ -0,0 +1,147 @@ +import { gt, and } from "@ember/object/computed"; +import { schedule } from "@ember/runloop"; +import Component from "@ember/component"; +import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { escape } from "pretty-text/sanitizer"; +import { isTesting } from "discourse-common/config/environment"; + +const MAX_COMPONENTS = 4; + +export default Component.extend({ + childrenExpanded: false, + classNames: ["themes-list-item"], + classNameBindings: ["theme.selected:selected"], + hasComponents: gt("children.length", 0), + displayComponents: and("hasComponents", "theme.isActive"), + displayHasMore: gt("theme.childThemes.length", MAX_COMPONENTS), + + click(e) { + if (!$(e.target).hasClass("others-count")) { + this.navigateToTheme(); + } + }, + + init() { + this._super(...arguments); + this.scheduleAnimation(); + }, + + @observes("theme.selected") + triggerAnimation() { + this.animate(); + }, + + scheduleAnimation() { + schedule("afterRender", () => { + this.animate(true); + }); + }, + + animate(isInitial) { + const $container = $(this.element); + const $list = $(this.element.querySelector(".components-list")); + if ($list.length === 0 || isTesting()) { + return; + } + const duration = 300; + if (this.get("theme.selected")) { + this.collapseComponentsList($container, $list, duration); + } else if (!isInitial) { + this.expandComponentsList($container, $list, duration); + } + }, + + @discourseComputed( + "theme.component", + "theme.childThemes.@each.name", + "theme.childThemes.length", + "childrenExpanded" + ) + children() { + const theme = this.theme; + let children = theme.get("childThemes"); + if (theme.get("component") || !children) { + return []; + } + children = this.childrenExpanded + ? children + : children.slice(0, MAX_COMPONENTS); + return children.map(t => { + const name = escape(t.name); + return t.enabled ? name : `${iconHTML("ban")} ${name}`; + }); + }, + + @discourseComputed("children") + childrenString(children) { + return children.join(", "); + }, + + @discourseComputed( + "theme.childThemes.length", + "theme.component", + "childrenExpanded", + "children.length" + ) + moreCount(childrenCount, component, expanded) { + if (component || !childrenCount || expanded) { + return 0; + } + return childrenCount - MAX_COMPONENTS; + }, + + expandComponentsList($container, $list, duration) { + $container.css("height", `${$container.height()}px`); + $list.css("display", ""); + $container.animate( + { + height: `${$container.height() + $list.outerHeight(true)}px` + }, + { + duration, + done: () => { + $list.css("display", ""); + $container.css("height", ""); + } + } + ); + $list.animate( + { + opacity: 1 + }, + { + duration + } + ); + }, + + collapseComponentsList($container, $list, duration) { + $container.animate( + { + height: `${$container.height() - $list.outerHeight(true)}px` + }, + { + duration, + done: () => { + $list.css("display", "none"); + $container.css("height", ""); + } + } + ); + $list.animate( + { + opacity: 0 + }, + { + duration + } + ); + }, + + actions: { + toggleChildrenExpanded() { + this.toggleProperty("childrenExpanded"); + } + } +}); diff --git a/app/assets/javascripts/admin/components/themes-list-item.js.es6 b/app/assets/javascripts/admin/components/themes-list-item.js.es6 deleted file mode 100644 index c292b80f99..0000000000 --- a/app/assets/javascripts/admin/components/themes-list-item.js.es6 +++ /dev/null @@ -1,147 +0,0 @@ -import { gt, and } from "@ember/object/computed"; -import { schedule } from "@ember/runloop"; -import Component from "@ember/component"; -import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { iconHTML } from "discourse-common/lib/icon-library"; -import { escape } from "pretty-text/sanitizer"; -import ENV from "discourse-common/config/environment"; - -const MAX_COMPONENTS = 4; - -export default Component.extend({ - childrenExpanded: false, - classNames: ["themes-list-item"], - classNameBindings: ["theme.selected:selected"], - hasComponents: gt("children.length", 0), - displayComponents: and("hasComponents", "theme.isActive"), - displayHasMore: gt("theme.childThemes.length", MAX_COMPONENTS), - - click(e) { - if (!$(e.target).hasClass("others-count")) { - this.navigateToTheme(); - } - }, - - init() { - this._super(...arguments); - this.scheduleAnimation(); - }, - - @observes("theme.selected") - triggerAnimation() { - this.animate(); - }, - - scheduleAnimation() { - schedule("afterRender", () => { - this.animate(true); - }); - }, - - animate(isInitial) { - const $container = $(this.element); - const $list = $(this.element.querySelector(".components-list")); - if ($list.length === 0 || ENV.environment === "test") { - return; - } - const duration = 300; - if (this.get("theme.selected")) { - this.collapseComponentsList($container, $list, duration); - } else if (!isInitial) { - this.expandComponentsList($container, $list, duration); - } - }, - - @discourseComputed( - "theme.component", - "theme.childThemes.@each.name", - "theme.childThemes.length", - "childrenExpanded" - ) - children() { - const theme = this.theme; - let children = theme.get("childThemes"); - if (theme.get("component") || !children) { - return []; - } - children = this.childrenExpanded - ? children - : children.slice(0, MAX_COMPONENTS); - return children.map(t => { - const name = escape(t.name); - return t.enabled ? name : `${iconHTML("ban")} ${name}`; - }); - }, - - @discourseComputed("children") - childrenString(children) { - return children.join(", "); - }, - - @discourseComputed( - "theme.childThemes.length", - "theme.component", - "childrenExpanded", - "children.length" - ) - moreCount(childrenCount, component, expanded) { - if (component || !childrenCount || expanded) { - return 0; - } - return childrenCount - MAX_COMPONENTS; - }, - - expandComponentsList($container, $list, duration) { - $container.css("height", `${$container.height()}px`); - $list.css("display", ""); - $container.animate( - { - height: `${$container.height() + $list.outerHeight(true)}px` - }, - { - duration, - done: () => { - $list.css("display", ""); - $container.css("height", ""); - } - } - ); - $list.animate( - { - opacity: 1 - }, - { - duration - } - ); - }, - - collapseComponentsList($container, $list, duration) { - $container.animate( - { - height: `${$container.height() - $list.outerHeight(true)}px` - }, - { - duration, - done: () => { - $list.css("display", "none"); - $container.css("height", ""); - } - } - ); - $list.animate( - { - opacity: 0 - }, - { - duration - } - ); - }, - - actions: { - toggleChildrenExpanded() { - this.toggleProperty("childrenExpanded"); - } - } -}); 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 b/app/assets/javascripts/admin/components/watched-word-form.js new file mode 100644 index 0000000000..96ddbffd7c --- /dev/null +++ b/app/assets/javascripts/admin/components/watched-word-form.js @@ -0,0 +1,99 @@ +import I18n from "I18n"; +import { isEmpty } from "@ember/utils"; +import { schedule } from "@ember/runloop"; +import Component from "@ember/component"; +import WatchedWord from "admin/models/watched-word"; +import discourseComputed, { + on, + observes +} from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNames: ["watched-word-form"], + formSubmitted: false, + actionKey: null, + showMessage: false, + + @discourseComputed("regularExpressions") + placeholderKey(regularExpressions) { + return ( + "admin.watched_words.form.placeholder" + + (regularExpressions ? "_regexp" : "") + ); + }, + + @observes("word") + removeMessage() { + if (this.showMessage && !isEmpty(this.word)) { + this.set("showMessage", false); + } + }, + + @discourseComputed("word") + isUniqueWord(word) { + const words = this.filteredContent || []; + const filtered = words.filter(content => content.action === this.actionKey); + return filtered.every( + content => content.word.toLowerCase() !== word.toLowerCase() + ); + }, + + actions: { + submit() { + if (!this.isUniqueWord) { + this.setProperties({ + showMessage: true, + message: I18n.t("admin.watched_words.form.exists") + }); + return; + } + + if (!this.formSubmitted) { + this.set("formSubmitted", true); + + const watchedWord = WatchedWord.create({ + word: this.word, + action: this.actionKey + }); + + watchedWord + .save() + .then(result => { + this.setProperties({ + word: "", + formSubmitted: false, + showMessage: true, + message: I18n.t("admin.watched_words.form.success") + }); + this.action(WatchedWord.create(result)); + schedule("afterRender", () => + this.element.querySelector(".watched-word-input").focus() + ); + }) + .catch(e => { + this.set("formSubmitted", false); + const msg = + e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors + ? I18n.t("generic_error_with_reason", { + error: e.jqXHR.responseJSON.errors.join(". ") + }) + : I18n.t("generic_error"); + bootbox.alert(msg, () => + this.element.querySelector(".watched-word-input").focus() + ); + }); + } + } + }, + + @on("didInsertElement") + _init() { + schedule("afterRender", () => { + $(this.element.querySelector(".watched-word-input")).keydown(e => { + if (e.keyCode === 13) { + this.send("submit"); + } + }); + }); + } +}); diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6 deleted file mode 100644 index fff5b86e26..0000000000 --- a/app/assets/javascripts/admin/components/watched-word-form.js.es6 +++ /dev/null @@ -1,98 +0,0 @@ -import { isEmpty } from "@ember/utils"; -import { schedule } from "@ember/runloop"; -import Component from "@ember/component"; -import WatchedWord from "admin/models/watched-word"; -import discourseComputed, { - on, - observes -} from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNames: ["watched-word-form"], - formSubmitted: false, - actionKey: null, - showMessage: false, - - @discourseComputed("regularExpressions") - placeholderKey(regularExpressions) { - return ( - "admin.watched_words.form.placeholder" + - (regularExpressions ? "_regexp" : "") - ); - }, - - @observes("word") - removeMessage() { - if (this.showMessage && !isEmpty(this.word)) { - this.set("showMessage", false); - } - }, - - @discourseComputed("word") - isUniqueWord(word) { - const words = this.filteredContent || []; - const filtered = words.filter(content => content.action === this.actionKey); - return filtered.every( - content => content.word.toLowerCase() !== word.toLowerCase() - ); - }, - - actions: { - submit() { - if (!this.isUniqueWord) { - this.setProperties({ - showMessage: true, - message: I18n.t("admin.watched_words.form.exists") - }); - return; - } - - if (!this.formSubmitted) { - this.set("formSubmitted", true); - - const watchedWord = WatchedWord.create({ - word: this.word, - action: this.actionKey - }); - - watchedWord - .save() - .then(result => { - this.setProperties({ - word: "", - formSubmitted: false, - showMessage: true, - message: I18n.t("admin.watched_words.form.success") - }); - this.action(WatchedWord.create(result)); - schedule("afterRender", () => - this.element.querySelector(".watched-word-input").focus() - ); - }) - .catch(e => { - this.set("formSubmitted", false); - const msg = - e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors - ? I18n.t("generic_error_with_reason", { - error: e.jqXHR.responseJSON.errors.join(". ") - }) - : I18n.t("generic_error"); - bootbox.alert(msg, () => - this.element.querySelector(".watched-word-input").focus() - ); - }); - } - } - }, - - @on("didInsertElement") - _init() { - schedule("afterRender", () => { - $(this.element.querySelector(".watched-word-input")).keydown(e => { - if (e.keyCode === 13) { - this.send("submit"); - } - }); - }); - } -}); diff --git a/app/assets/javascripts/admin/components/watched-word-uploader.js b/app/assets/javascripts/admin/components/watched-word-uploader.js new file mode 100644 index 0000000000..dceecea228 --- /dev/null +++ b/app/assets/javascripts/admin/components/watched-word-uploader.js @@ -0,0 +1,28 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; +import Component from "@ember/component"; +import UploadMixin from "discourse/mixins/upload"; + +export default Component.extend(UploadMixin, { + type: "txt", + classNames: "watched-words-uploader", + uploadUrl: "/admin/logs/watched_words/upload", + addDisabled: alias("uploading"), + + validateUploadedFilesOptions() { + return { skipValidation: true }; + }, + + @discourseComputed("actionKey") + data(actionKey) { + return { action_key: actionKey }; + }, + + uploadDone() { + if (this) { + bootbox.alert(I18n.t("admin.watched_words.form.upload_successful")); + this.done(); + } + } +}); diff --git a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 deleted file mode 100644 index 05dc41c207..0000000000 --- a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { alias } from "@ember/object/computed"; -import Component from "@ember/component"; -import UploadMixin from "discourse/mixins/upload"; - -export default Component.extend(UploadMixin, { - type: "txt", - classNames: "watched-words-uploader", - uploadUrl: "/admin/logs/watched_words/upload", - addDisabled: alias("uploading"), - - validateUploadedFilesOptions() { - return { skipValidation: true }; - }, - - @discourseComputed("actionKey") - data(actionKey) { - return { action_key: actionKey }; - }, - - uploadDone() { - if (this) { - bootbox.alert(I18n.t("admin.watched_words.form.upload_successful")); - this.done(); - } - } -}); 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..eaddcfcd82 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-api-keys-new.js @@ -0,0 +1,41 @@ +import I18n from "I18n"; +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..c3b43450b1 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js @@ -0,0 +1,59 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/controllers/admin-badges-award.js new file mode 100644 index 0000000000..d7945b15c7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-badges-award.js @@ -0,0 +1,38 @@ +import I18n from "I18n"; +import Controller from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default Controller.extend({ + saving: false, + replaceBadgeOwners: false, + + actions: { + massAward() { + const file = document.querySelector("#massAwardCSVUpload").files[0]; + + if (this.model && file) { + const options = { + type: "POST", + processData: false, + contentType: false, + data: new FormData() + }; + + options.data.append("file", file); + options.data.append("replace_badge_owners", this.replaceBadgeOwners); + + this.set("saving", true); + + ajax(`/admin/badges/award/${this.model.id}`, options) + .then(() => { + bootbox.alert(I18n.t("admin.badges.mass_award.success")); + }) + .catch(popupAjaxError) + .finally(() => this.set("saving", false)); + } else { + bootbox.alert(I18n.t("admin.badges.mass_award.aborted")); + } + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 deleted file mode 100644 index ef141ccac1..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Controller.extend({ - saving: false, - replaceBadgeOwners: false, - - actions: { - massAward() { - const file = document.querySelector("#massAwardCSVUpload").files[0]; - - if (this.model && file) { - const options = { - type: "POST", - processData: false, - contentType: false, - data: new FormData() - }; - - options.data.append("file", file); - options.data.append("replace_badge_owners", this.replaceBadgeOwners); - - this.set("saving", true); - - ajax(`/admin/badges/award/${this.model.id}`, options) - .then(() => { - bootbox.alert(I18n.t("admin.badges.mass_award.success")); - }) - .catch(popupAjaxError) - .finally(() => this.set("saving", false)); - } else { - bootbox.alert(I18n.t("admin.badges.mass_award.aborted")); - } - } - } -}); 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..6a44fe4ae4 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-badges-show.js @@ -0,0 +1,167 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js new file mode 100644 index 0000000000..61f6e5d6e7 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js @@ -0,0 +1,91 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { later } from "@ember/runloop"; +import Controller from "@ember/controller"; + +export default Controller.extend({ + @discourseComputed("model.colors", "onlyOverridden") + colors(allColors, onlyOverridden) { + if (onlyOverridden) { + return allColors.filter(color => color.get("overridden")); + } else { + return allColors; + } + }, + + actions: { + revert: function(color) { + color.revert(); + }, + + undo: function(color) { + color.undo(); + }, + + copyToClipboard() { + $(".table.colors").hide(); + let area = $(""); + $(".table.colors").after(area); + area.text(this.model.schemeJson()); + let range = document.createRange(); + range.selectNode(area[0]); + window.getSelection().addRange(range); + let successful = document.execCommand("copy"); + if (successful) { + this.set( + "model.savingStatus", + I18n.t("admin.customize.copied_to_clipboard") + ); + } else { + this.set( + "model.savingStatus", + I18n.t("admin.customize.copy_to_clipboard_error") + ); + } + + later(() => { + this.set("model.savingStatus", null); + }, 2000); + + window.getSelection().removeAllRanges(); + + $(".table.colors").show(); + $(area).remove(); + }, + + copy() { + const newColorScheme = this.model.copy(); + newColorScheme.set( + "name", + I18n.t("admin.customize.colors.copy_name_prefix") + + " " + + this.get("model.name") + ); + newColorScheme.save().then(() => { + this.allColors.pushObject(newColorScheme); + this.replaceRoute("adminCustomize.colors.show", newColorScheme); + }); + }, + + save: function() { + this.model.save(); + }, + + destroy: function() { + const model = this.model; + return bootbox.confirm( + I18n.t("admin.customize.colors.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + model.destroy().then(() => { + this.allColors.removeObject(model); + this.replaceRoute("adminCustomize.colors"); + }); + } + } + ); + } + } +}); 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.es6 deleted file mode 100644 index 9cdca3f098..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 +++ /dev/null @@ -1,90 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { later } from "@ember/runloop"; -import Controller from "@ember/controller"; - -export default Controller.extend({ - @discourseComputed("model.colors", "onlyOverridden") - colors(allColors, onlyOverridden) { - if (onlyOverridden) { - return allColors.filter(color => color.get("overridden")); - } else { - return allColors; - } - }, - - actions: { - revert: function(color) { - color.revert(); - }, - - undo: function(color) { - color.undo(); - }, - - copyToClipboard() { - $(".table.colors").hide(); - let area = $(""); - $(".table.colors").after(area); - area.text(this.model.schemeJson()); - let range = document.createRange(); - range.selectNode(area[0]); - window.getSelection().addRange(range); - let successful = document.execCommand("copy"); - if (successful) { - this.set( - "model.savingStatus", - I18n.t("admin.customize.copied_to_clipboard") - ); - } else { - this.set( - "model.savingStatus", - I18n.t("admin.customize.copy_to_clipboard_error") - ); - } - - later(() => { - this.set("model.savingStatus", null); - }, 2000); - - window.getSelection().removeAllRanges(); - - $(".table.colors").show(); - $(area).remove(); - }, - - copy() { - var newColorScheme = Ember.copy(this.model, true); - newColorScheme.set( - "name", - I18n.t("admin.customize.colors.copy_name_prefix") + - " " + - this.get("model.name") - ); - newColorScheme.save().then(() => { - this.allColors.pushObject(newColorScheme); - this.replaceRoute("adminCustomize.colors.show", newColorScheme); - }); - }, - - save: function() { - this.model.save(); - }, - - destroy: function() { - const model = this.model; - return bootbox.confirm( - I18n.t("admin.customize.colors.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - model.destroy().then(() => { - this.allColors.removeObject(model); - this.replaceRoute("adminCustomize.colors"); - }); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js b/app/assets/javascripts/admin/controllers/admin-customize-colors.js new file mode 100644 index 0000000000..062121fc47 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors.js @@ -0,0 +1,49 @@ +import I18n from "I18n"; +import EmberObject from "@ember/object"; +import Controller from "@ember/controller"; +import showModal from "discourse/lib/show-modal"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Controller.extend({ + @discourseComputed("model.@each.id") + baseColorScheme() { + return this.model.findBy("is_base", true); + }, + + @discourseComputed("model.@each.id") + baseColorSchemes() { + return this.model.filterBy("is_base", true); + }, + + @discourseComputed("baseColorScheme") + baseColors(baseColorScheme) { + const baseColorsHash = EmberObject.create({}); + baseColorScheme.get("colors").forEach(color => { + baseColorsHash.set(color.get("name"), color); + }); + return baseColorsHash; + }, + + actions: { + newColorSchemeWithBase(baseKey) { + const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey); + const newColorScheme = base.copy(); + newColorScheme.setProperties({ + name: I18n.t("admin.customize.colors.new_name"), + base_scheme_id: base.get("base_scheme_id") + }); + newColorScheme.save().then(() => { + this.model.pushObject(newColorScheme); + newColorScheme.set("savingStatus", null); + this.replaceRoute("adminCustomize.colors.show", newColorScheme); + }); + }, + + newColorScheme() { + showModal("admin-color-scheme-select-base", { + model: this.baseColorSchemes, + admin: true + }); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 deleted file mode 100644 index 869918bebb..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -import EmberObject from "@ember/object"; -import Controller from "@ember/controller"; -import showModal from "discourse/lib/show-modal"; -import discourseComputed from "discourse-common/utils/decorators"; - -export default Controller.extend({ - @discourseComputed("model.@each.id") - baseColorScheme() { - return this.model.findBy("is_base", true); - }, - - @discourseComputed("model.@each.id") - baseColorSchemes() { - return this.model.filterBy("is_base", true); - }, - - @discourseComputed("baseColorScheme") - baseColors(baseColorScheme) { - const baseColorsHash = EmberObject.create({}); - baseColorScheme.get("colors").forEach(color => { - baseColorsHash.set(color.get("name"), color); - }); - return baseColorsHash; - }, - - actions: { - newColorSchemeWithBase(baseKey) { - const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey); - const newColorScheme = Ember.copy(base, true); - newColorScheme.setProperties({ - name: I18n.t("admin.customize.colors.new_name"), - base_scheme_id: base.get("base_scheme_id") - }); - newColorScheme.save().then(() => { - this.model.pushObject(newColorScheme); - newColorScheme.set("savingStatus", null); - this.replaceRoute("adminCustomize.colors.show", newColorScheme); - }); - }, - - newColorScheme() { - showModal("admin-color-scheme-select-base", { - model: this.baseColorSchemes, - admin: true - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js b/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js new file mode 100644 index 0000000000..0daf5c7973 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js @@ -0,0 +1,35 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import Controller from "@ember/controller"; + +export default Controller.extend({ + @discourseComputed("model.isSaving") + saveButtonText(isSaving) { + return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); + }, + + @discourseComputed("model.changed", "model.isSaving") + saveDisabled(changed, isSaving) { + return !changed || isSaving; + }, + + actions: { + save() { + if (!this.model.saving) { + this.set("saving", true); + this.model + .update(this.model.getProperties("html", "css")) + .catch(e => { + const msg = + e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors + ? I18n.t("admin.customize.email_style.save_error_with_reason", { + error: e.jqXHR.responseJSON.errors.join(". ") + }) + : I18n.t("generic_error"); + bootbox.alert(msg); + }) + .finally(() => this.set("model.changed", false)); + } + } + } +}); 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.es6 deleted file mode 100644 index d534792b00..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js.es6 +++ /dev/null @@ -1,34 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Controller from "@ember/controller"; - -export default Controller.extend({ - @discourseComputed("model.isSaving") - saveButtonText(isSaving) { - return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); - }, - - @discourseComputed("model.changed", "model.isSaving") - saveDisabled(changed, isSaving) { - return !changed || isSaving; - }, - - actions: { - save() { - if (!this.model.saving) { - this.set("saving", true); - this.model - .update(this.model.getProperties("html", "css")) - .catch(e => { - const msg = - e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors - ? I18n.t("admin.customize.email_style.save_error_with_reason", { - error: e.jqXHR.responseJSON.errors.join(". ") - }) - : I18n.t("generic_error"); - bootbox.alert(msg); - }) - .finally(() => this.set("model.changed", false)); - } - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js new file mode 100644 index 0000000000..f6e19c6368 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js @@ -0,0 +1,57 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import Controller from "@ember/controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; + +export default Controller.extend(bufferedProperty("emailTemplate"), { + saved: false, + + @discourseComputed("buffered.body", "buffered.subject") + saveDisabled(body, subject) { + return ( + this.emailTemplate.body === body && this.emailTemplate.subject === subject + ); + }, + + @discourseComputed("buffered") + hasMultipleSubjects(buffered) { + if (buffered.getProperties("subject")["subject"]) { + return false; + } else { + return buffered.getProperties("id")["id"]; + } + }, + + actions: { + saveChanges() { + this.set("saved", false); + const buffered = this.buffered; + this.emailTemplate + .save(buffered.getProperties("subject", "body")) + .then(() => { + this.set("saved", true); + }) + .catch(popupAjaxError); + }, + + revertChanges() { + this.set("saved", false); + bootbox.confirm( + I18n.t("admin.customize.email_templates.revert_confirm"), + result => { + if (result) { + this.emailTemplate + .revert() + .then(props => { + const buffered = this.buffered; + buffered.setProperties(props); + this.commitBuffer(); + }) + .catch(popupAjaxError); + } + } + ); + } + } +}); 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.es6 deleted file mode 100644 index 3617edca75..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 +++ /dev/null @@ -1,56 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; - -export default Controller.extend(bufferedProperty("emailTemplate"), { - saved: false, - - @discourseComputed("buffered.body", "buffered.subject") - saveDisabled(body, subject) { - return ( - this.emailTemplate.body === body && this.emailTemplate.subject === subject - ); - }, - - @discourseComputed("buffered") - hasMultipleSubjects(buffered) { - if (buffered.getProperties("subject")["subject"]) { - return false; - } else { - return buffered.getProperties("id")["id"]; - } - }, - - actions: { - saveChanges() { - this.set("saved", false); - const buffered = this.buffered; - this.emailTemplate - .save(buffered.getProperties("subject", "body")) - .then(() => { - this.set("saved", true); - }) - .catch(popupAjaxError); - }, - - revertChanges() { - this.set("saved", false); - bootbox.confirm( - I18n.t("admin.customize.email_templates.revert_confirm"), - result => { - if (result) { - this.emailTemplate - .revert() - .then(props => { - const buffered = this.buffered; - buffered.setProperties(props); - this.commitBuffer(); - }) - .catch(popupAjaxError); - } - } - ); - } - } -}); 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 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js new file mode 100644 index 0000000000..a606505692 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js @@ -0,0 +1,68 @@ +import I18n from "I18n"; +import Controller from "@ember/controller"; +import { url } from "discourse/lib/computed"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Controller.extend({ + section: null, + currentTarget: 0, + maximized: false, + previewUrl: url("model.id", "/admin/themes/%@/preview"), + showAdvanced: false, + editRouteName: "adminCustomizeThemes.edit", + showRouteName: "adminCustomizeThemes.show", + + setTargetName: function(name) { + const target = this.get("model.targets").find(t => t.name === name); + this.set("currentTarget", target && target.id); + }, + + @discourseComputed("currentTarget") + currentTargetName(id) { + const target = this.get("model.targets").find( + t => t.id === parseInt(id, 10) + ); + return target && target.name; + }, + + @discourseComputed("model.isSaving") + saveButtonText(isSaving) { + return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); + }, + + @discourseComputed("model.changed", "model.isSaving") + saveDisabled(changed, isSaving) { + return !changed || isSaving; + }, + + actions: { + save() { + this.set("saving", true); + this.model.saveChanges("theme_fields").finally(() => { + this.set("saving", false); + }); + }, + + fieldAdded(target, name) { + this.replaceRoute(this.editRouteName, this.get("model.id"), target, name); + }, + + onlyOverriddenChanged(onlyShowOverridden) { + if (onlyShowOverridden) { + if (!this.model.hasEdited(this.currentTargetName, this.fieldName)) { + let firstTarget = this.get("model.targets").find(t => t.edited); + let firstField = this.get(`model.fields.${firstTarget.name}`).find( + f => f.edited + ); + + this.replaceRoute( + this.editRouteName, + this.get("model.id"), + firstTarget.name, + firstField.name + ); + } + } + } + } +}); 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.es6 deleted file mode 100644 index c3c0f4fc99..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ /dev/null @@ -1,67 +0,0 @@ -import Controller from "@ember/controller"; -import { url } from "discourse/lib/computed"; -import discourseComputed from "discourse-common/utils/decorators"; - -export default Controller.extend({ - section: null, - currentTarget: 0, - maximized: false, - previewUrl: url("model.id", "/admin/themes/%@/preview"), - showAdvanced: false, - editRouteName: "adminCustomizeThemes.edit", - showRouteName: "adminCustomizeThemes.show", - - setTargetName: function(name) { - const target = this.get("model.targets").find(t => t.name === name); - this.set("currentTarget", target && target.id); - }, - - @discourseComputed("currentTarget") - currentTargetName(id) { - const target = this.get("model.targets").find( - t => t.id === parseInt(id, 10) - ); - return target && target.name; - }, - - @discourseComputed("model.isSaving") - saveButtonText(isSaving) { - return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); - }, - - @discourseComputed("model.changed", "model.isSaving") - saveDisabled(changed, isSaving) { - return !changed || isSaving; - }, - - actions: { - save() { - this.set("saving", true); - this.model.saveChanges("theme_fields").finally(() => { - this.set("saving", false); - }); - }, - - fieldAdded(target, name) { - this.replaceRoute(this.editRouteName, this.get("model.id"), target, name); - }, - - onlyOverriddenChanged(onlyShowOverridden) { - if (onlyShowOverridden) { - if (!this.model.hasEdited(this.currentTargetName, this.fieldName)) { - let firstTarget = this.get("model.targets").find(t => t.edited); - let firstField = this.get(`model.fields.${firstTarget.name}`).find( - f => f.edited - ); - - this.replaceRoute( - this.editRouteName, - this.get("model.id"), - firstTarget.name, - firstField.name - ); - } - } - } - } -}); 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..2c5ad32a18 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js @@ -0,0 +1,388 @@ +import I18n from "I18n"; +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"), + extraFiles: filterBy("model.theme_fields", "target", "extra_js"), + + @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 b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js new file mode 100644 index 0000000000..2030c1e613 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js @@ -0,0 +1,155 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { makeArray } from "discourse-common/lib/helpers"; +import { inject } from "@ember/controller"; +import Controller from "@ember/controller"; +import { setting } from "discourse/lib/computed"; +import AdminDashboard from "admin/models/admin-dashboard"; +import Report from "admin/models/report"; +import PeriodComputationMixin from "admin/mixins/period-computation"; +import { computed } from "@ember/object"; +import getURL from "discourse-common/lib/get-url"; + +function staticReport(reportType) { + return computed("reports.[]", function() { + return makeArray(this.reports).find(report => report.type === reportType); + }); +} + +export default Controller.extend(PeriodComputationMixin, { + isLoading: false, + dashboardFetchedAt: null, + exceptionController: inject("exception"), + logSearchQueriesEnabled: setting("log_search_queries"), + + @discourseComputed("siteSettings.dashboard_general_tab_activity_metrics") + activityMetrics(metrics) { + return (metrics || "").split("|").filter(Boolean); + }, + + hiddenReports: computed("siteSettings.dashboard_hidden_reports", function() { + return (this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean); + }), + + isActivityMetricsVisible: computed( + "activityMetrics", + "hiddenReports", + function() { + return ( + this.activityMetrics.length && + this.activityMetrics.some(x => !this.hiddenReports.includes(x)) + ); + } + ), + + isSearchReportsVisible: computed("hiddenReports", function() { + return ["top_referred_topics", "trending_search"].some( + x => !this.hiddenReports.includes(x) + ); + }), + + isCommunityHealthVisible: computed("hiddenReports", function() { + return [ + "consolidated_page_views", + "signups", + "topics", + "posts", + "dau_by_mau", + "daily_engaged_users", + "new_contributors" + ].some(x => !this.hiddenReports.includes(x)); + }), + + @discourseComputed + activityMetricsFilters() { + return { + startDate: this.lastMonth, + endDate: this.today + }; + }, + + @discourseComputed + topReferredTopicsOptions() { + return { + table: { total: false, limit: 8 } + }; + }, + + @discourseComputed + topReferredTopicsFilters() { + return { + startDate: moment() + .subtract(6, "days") + .startOf("day"), + endDate: this.today + }; + }, + + @discourseComputed + trendingSearchFilters() { + return { + startDate: moment() + .subtract(1, "month") + .startOf("day"), + endDate: this.today + }; + }, + + @discourseComputed + trendingSearchOptions() { + return { + table: { total: false, limit: 8 } + }; + }, + + @discourseComputed + trendingSearchDisabledLabel() { + return I18n.t("admin.dashboard.reports.trending_search.disabled", { + basePath: getURL("/") + }); + }, + + usersByTypeReport: staticReport("users_by_type"), + usersByTrustLevelReport: staticReport("users_by_trust_level"), + storageReport: staticReport("storage_report"), + + fetchDashboard() { + if (this.isLoading) return; + + if ( + !this.dashboardFetchedAt || + moment() + .subtract(30, "minutes") + .toDate() > this.dashboardFetchedAt + ) { + this.set("isLoading", true); + + AdminDashboard.fetchGeneral() + .then(adminDashboardModel => { + this.setProperties({ + dashboardFetchedAt: new Date(), + model: adminDashboardModel, + reports: makeArray(adminDashboardModel.reports).map(x => + Report.create(x) + ) + }); + }) + .catch(e => { + this.exceptionController.set("thrown", e.jqXHR); + this.replaceRoute("exception"); + }) + .finally(() => this.set("isLoading", false)); + } + }, + + @discourseComputed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + + _reportsForPeriodURL(period) { + return getURL(`/admin?period=${period}`); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-general.js.es6 deleted file mode 100644 index b77e3e0288..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-general.js.es6 +++ /dev/null @@ -1,119 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { makeArray } from "discourse-common/lib/helpers"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import { setting } from "discourse/lib/computed"; -import AdminDashboard from "admin/models/admin-dashboard"; -import Report from "admin/models/report"; -import PeriodComputationMixin from "admin/mixins/period-computation"; -import { computed } from "@ember/object"; - -function staticReport(reportType) { - return computed("reports.[]", function() { - return makeArray(this.reports).find(report => report.type === reportType); - }); -} - -export default Controller.extend(PeriodComputationMixin, { - isLoading: false, - dashboardFetchedAt: null, - exceptionController: inject("exception"), - logSearchQueriesEnabled: setting("log_search_queries"), - basePath: Discourse.BaseUri, - - @discourseComputed("siteSettings.dashboard_general_tab_activity_metrics") - activityMetrics(metrics) { - return (metrics || "").split("|").filter(m => m); - }, - - @discourseComputed - activityMetricsFilters() { - return { - startDate: this.lastMonth, - endDate: this.today - }; - }, - - @discourseComputed - topReferredTopicsOptions() { - return { - table: { total: false, limit: 8 } - }; - }, - - @discourseComputed - topReferredTopicsFilters() { - return { - startDate: moment() - .subtract(6, "days") - .startOf("day"), - endDate: this.today - }; - }, - - @discourseComputed - trendingSearchFilters() { - return { - startDate: moment() - .subtract(1, "month") - .startOf("day"), - endDate: this.today - }; - }, - - @discourseComputed - trendingSearchOptions() { - return { - table: { total: false, limit: 8 } - }; - }, - - @discourseComputed - trendingSearchDisabledLabel() { - return I18n.t("admin.dashboard.reports.trending_search.disabled", { - basePath: Discourse.BaseUri - }); - }, - - usersByTypeReport: staticReport("users_by_type"), - usersByTrustLevelReport: staticReport("users_by_trust_level"), - storageReport: staticReport("storage_report"), - - fetchDashboard() { - if (this.isLoading) return; - - if ( - !this.dashboardFetchedAt || - moment() - .subtract(30, "minutes") - .toDate() > this.dashboardFetchedAt - ) { - this.set("isLoading", true); - - AdminDashboard.fetchGeneral() - .then(adminDashboardModel => { - this.setProperties({ - dashboardFetchedAt: new Date(), - model: adminDashboardModel, - reports: makeArray(adminDashboardModel.reports).map(x => - Report.create(x) - ) - }); - }) - .catch(e => { - this.exceptionController.set("thrown", e.jqXHR); - this.replaceRoute("exception"); - }) - .finally(() => this.set("isLoading", false)); - } - }, - - @discourseComputed("startDate", "endDate") - filters(startDate, endDate) { - return { startDate, endDate }; - }, - - _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin?period=${period}`); - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js new file mode 100644 index 0000000000..f37d8606af --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js @@ -0,0 +1,51 @@ +import getURL from "discourse-common/lib/get-url"; +import discourseComputed from "discourse-common/utils/decorators"; +import Controller from "@ember/controller"; +import PeriodComputationMixin from "admin/mixins/period-computation"; +import { computed } from "@ember/object"; + +export default Controller.extend(PeriodComputationMixin, { + @discourseComputed + flagsStatusOptions() { + return { + table: { + total: false, + perPage: 10 + } + }; + }, + + isModeratorsActivityVisible: computed( + "siteSettings.dashboard_hidden_reports", + function() { + return !(this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean) + .includes("moderators_activity"); + } + ), + + @discourseComputed + userFlaggingRatioOptions() { + return { + table: { + total: false, + perPage: 10 + } + }; + }, + + @discourseComputed("startDate", "endDate") + filters(startDate, endDate) { + return { startDate, endDate }; + }, + + @discourseComputed("lastWeek", "endDate") + lastWeekfilters(startDate, endDate) { + return { startDate, endDate }; + }, + + _reportsForPeriodURL(period) { + return getURL(`/admin/dashboard/moderation?period=${period}`); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js.es6 deleted file mode 100644 index 8925825fba..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-moderation.js.es6 +++ /dev/null @@ -1,39 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Controller from "@ember/controller"; -import PeriodComputationMixin from "admin/mixins/period-computation"; - -export default Controller.extend(PeriodComputationMixin, { - @discourseComputed - flagsStatusOptions() { - return { - table: { - total: false, - perPage: 10 - } - }; - }, - - @discourseComputed - userFlaggingRatioOptions() { - return { - table: { - total: false, - perPage: 10 - } - }; - }, - - @discourseComputed("startDate", "endDate") - filters(startDate, endDate) { - return { startDate, endDate }; - }, - - @discourseComputed("lastWeek", "endDate") - lastWeekfilters(startDate, endDate) { - return { startDate, endDate }; - }, - - _reportsForPeriodURL(period) { - return Discourse.getURL(`/admin/dashboard/moderation?period=${period}`); - } -}); 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..c096c3e763 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js @@ -0,0 +1,44 @@ +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", + "siteSettings.dashboard_hidden_reports" + ) + filterReports(reports, filter) { + if (filter) { + filter = filter.toLowerCase(); + reports = reports.filter(report => { + return ( + (get(report, "title") || "").toLowerCase().indexOf(filter) > -1 || + (get(report, "description") || "").toLowerCase().indexOf(filter) > -1 + ); + }); + } + + const hiddenReports = (this.siteSettings.dashboard_hidden_reports || "") + .split("|") + .filter(Boolean); + reports = reports.filter(report => !hiddenReports.includes(report.type)); + + return reports; + }, + + 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 b/app/assets/javascripts/admin/controllers/admin-dashboard.js new file mode 100644 index 0000000000..5946a1736d --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js @@ -0,0 +1,110 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import Controller, { inject } from "@ember/controller"; +import { setting } from "discourse/lib/computed"; +import { computed } from "@ember/object"; +import AdminDashboard from "admin/models/admin-dashboard"; +import VersionCheck from "admin/models/version-check"; + +const PROBLEMS_CHECK_MINUTES = 1; + +export default Controller.extend({ + isLoading: false, + dashboardFetchedAt: null, + exceptionController: inject("exception"), + showVersionChecks: setting("version_checks"), + + @discourseComputed("problems.length") + foundProblems(problemsLength) { + return this.currentUser.get("admin") && (problemsLength || 0) > 0; + }, + + visibleTabs: computed("siteSettings.dashboard_visible_tabs", function() { + return (this.siteSettings.dashboard_visible_tabs || "") + .split("|") + .filter(Boolean); + }), + + isModerationTabVisible: computed("visibleTabs", function() { + return this.visibleTabs.includes("moderation"); + }), + + isSecurityTabVisible: computed("visibleTabs", function() { + return this.visibleTabs.includes("security"); + }), + + isReportsTabVisible: computed("visibleTabs", function() { + return this.visibleTabs.includes("reports"); + }), + + fetchProblems() { + if (this.isLoadingProblems) return; + + if ( + !this.problemsFetchedAt || + moment() + .subtract(PROBLEMS_CHECK_MINUTES, "minutes") + .toDate() > this.problemsFetchedAt + ) { + this._loadProblems(); + } + }, + + fetchDashboard() { + const versionChecks = this.siteSettings.version_checks; + + if (this.isLoading || !versionChecks) return; + + if ( + !this.dashboardFetchedAt || + moment() + .subtract(30, "minutes") + .toDate() > this.dashboardFetchedAt + ) { + this.set("isLoading", true); + + AdminDashboard.fetch() + .then(model => { + let properties = { + dashboardFetchedAt: new Date() + }; + + if (versionChecks) { + properties.versionCheck = VersionCheck.create(model.version_check); + } + + this.setProperties(properties); + }) + .catch(e => { + this.exceptionController.set("thrown", e.jqXHR); + this.replaceRoute("exception"); + }) + .finally(() => { + this.set("isLoading", false); + }); + } + }, + + _loadProblems() { + this.setProperties({ + loadingProblems: true, + problemsFetchedAt: new Date() + }); + + AdminDashboard.fetchProblems() + .then(model => this.set("problems", model.problems)) + .finally(() => this.set("loadingProblems", false)); + }, + + @discourseComputed("problemsFetchedAt") + problemsTimestamp(problemsFetchedAt) { + return moment(problemsFetchedAt) + .locale("en") + .format("LLL"); + }, + + actions: { + refreshProblems() { + this._loadProblems(); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 deleted file mode 100644 index bd8561abc1..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ /dev/null @@ -1,92 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { inject } from "@ember/controller"; -import Controller from "@ember/controller"; -import { setting } from "discourse/lib/computed"; -import AdminDashboard from "admin/models/admin-dashboard"; -import VersionCheck from "admin/models/version-check"; - -const PROBLEMS_CHECK_MINUTES = 1; - -export default Controller.extend({ - isLoading: false, - dashboardFetchedAt: null, - exceptionController: inject("exception"), - showVersionChecks: setting("version_checks"), - - @discourseComputed("problems.length") - foundProblems(problemsLength) { - return this.currentUser.get("admin") && (problemsLength || 0) > 0; - }, - - fetchProblems() { - if (this.isLoadingProblems) return; - - if ( - !this.problemsFetchedAt || - moment() - .subtract(PROBLEMS_CHECK_MINUTES, "minutes") - .toDate() > this.problemsFetchedAt - ) { - this._loadProblems(); - } - }, - - fetchDashboard() { - const versionChecks = this.siteSettings.version_checks; - - if (this.isLoading || !versionChecks) return; - - if ( - !this.dashboardFetchedAt || - moment() - .subtract(30, "minutes") - .toDate() > this.dashboardFetchedAt - ) { - this.set("isLoading", true); - - AdminDashboard.fetch() - .then(model => { - let properties = { - dashboardFetchedAt: new Date() - }; - - if (versionChecks) { - properties.versionCheck = VersionCheck.create(model.version_check); - } - - this.setProperties(properties); - }) - .catch(e => { - this.exceptionController.set("thrown", e.jqXHR); - this.replaceRoute("exception"); - }) - .finally(() => { - this.set("isLoading", false); - }); - } - }, - - _loadProblems() { - this.setProperties({ - loadingProblems: true, - problemsFetchedAt: new Date() - }); - - AdminDashboard.fetchProblems() - .then(model => this.set("problems", model.problems)) - .finally(() => this.set("loadingProblems", false)); - }, - - @discourseComputed("problemsFetchedAt") - problemsTimestamp(problemsFetchedAt) { - return moment(problemsFetchedAt) - .locale("en") - .format("LLL"); - }, - - actions: { - refreshProblems() { - this._loadProblems(); - } - } -}); 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 b/app/assets/javascripts/admin/controllers/admin-email-index.js new file mode 100644 index 0000000000..8fad5ff034 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-email-index.js @@ -0,0 +1,58 @@ +import I18n from "I18n"; +import { empty } from "@ember/object/computed"; +import Controller from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; +import { observes } from "discourse-common/utils/decorators"; + +export default Controller.extend({ + /** + Is the "send test email" button disabled? + + @property sendTestEmailDisabled + **/ + sendTestEmailDisabled: empty("testEmailAddress"), + + /** + Clears the 'sentTestEmail' property on successful send. + + @method testEmailAddressChanged + **/ + @observes("testEmailAddress") + testEmailAddressChanged: function() { + this.set("sentTestEmail", false); + }, + + actions: { + /** + Sends a test email to the currently entered email address + + @method sendTestEmail + **/ + sendTestEmail: function() { + this.setProperties({ + sendingEmail: true, + sentTestEmail: false + }); + + ajax("/admin/email/test", { + type: "POST", + data: { email_address: this.testEmailAddress } + }) + .then(response => + this.set("sentTestEmailMessage", response.sent_test_email_message) + ) + .catch(e => { + if (e.responseJSON && e.responseJSON.errors) { + bootbox.alert( + I18n.t("admin.email.error", { + server_error: e.responseJSON.errors[0] + }) + ); + } else { + bootbox.alert(I18n.t("admin.email.test_error")); + } + }) + .finally(() => this.set("sendingEmail", false)); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 deleted file mode 100644 index 3433f6f55f..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 +++ /dev/null @@ -1,57 +0,0 @@ -import { empty } from "@ember/object/computed"; -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import { observes } from "discourse-common/utils/decorators"; - -export default Controller.extend({ - /** - Is the "send test email" button disabled? - - @property sendTestEmailDisabled - **/ - sendTestEmailDisabled: empty("testEmailAddress"), - - /** - Clears the 'sentTestEmail' property on successful send. - - @method testEmailAddressChanged - **/ - @observes("testEmailAddress") - testEmailAddressChanged: function() { - this.set("sentTestEmail", false); - }, - - actions: { - /** - Sends a test email to the currently entered email address - - @method sendTestEmail - **/ - sendTestEmail: function() { - this.setProperties({ - sendingEmail: true, - sentTestEmail: false - }); - - ajax("/admin/email/test", { - type: "POST", - data: { email_address: this.testEmailAddress } - }) - .then(response => - this.set("sentTestEmailMessage", response.sent_test_email_message) - ) - .catch(e => { - if (e.responseJSON && e.responseJSON.errors) { - bootbox.alert( - I18n.t("admin.email.error", { - server_error: e.responseJSON.errors[0] - }) - ); - } else { - bootbox.alert(I18n.t("admin.email.test_error")); - } - }) - .finally(() => this.set("sendingEmail", false)); - } - } -}); 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..96b9e1ff8e --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-emojis.js @@ -0,0 +1,75 @@ +import I18n from "I18n"; +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..73a780e278 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js @@ -0,0 +1,137 @@ +import I18n from "I18n"; +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..059267ca40 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js @@ -0,0 +1,132 @@ +import I18n from "I18n"; +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..23c47f5739 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-permalinks.js @@ -0,0 +1,59 @@ +import I18n from "I18n"; +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); + }, + + copyUrl(pl) { + let linkElement = document.querySelector(`#admin-permalink-${pl.id}`); + let textArea = document.createElement("textarea"); + textArea.value = linkElement.textContent; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand("Copy"); + textArea.remove(); + }, + + 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 b/app/assets/javascripts/admin/controllers/admin-search-logs-index.js new file mode 100644 index 0000000000..ab58efbfcb --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-search-logs-index.js @@ -0,0 +1,25 @@ +import I18n from "I18n"; +import Controller from "@ember/controller"; +export const DEFAULT_PERIOD = "yearly"; + +export default Controller.extend({ + loading: false, + period: DEFAULT_PERIOD, + searchType: "all", + + init() { + this._super(...arguments); + + this.searchTypeOptions = [ + { + id: "all", + name: I18n.t("admin.logs.search_logs.types.all_search_types") + }, + { id: "header", name: I18n.t("admin.logs.search_logs.types.header") }, + { + id: "full_page", + name: I18n.t("admin.logs.search_logs.types.full_page") + } + ]; + } +}); 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.es6 deleted file mode 100644 index 397b4c9b9e..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-search-logs-index.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -import Controller from "@ember/controller"; -export const DEFAULT_PERIOD = "yearly"; - -export default Controller.extend({ - loading: false, - period: DEFAULT_PERIOD, - searchType: "all", - - init() { - this._super(...arguments); - - this.searchTypeOptions = [ - { - id: "all", - name: I18n.t("admin.logs.search_logs.types.all_search_types") - }, - { id: "header", name: I18n.t("admin.logs.search_logs.types.header") }, - { - id: "full_page", - name: I18n.t("admin.logs.search_logs.types.full_page") - } - ]; - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-search-logs-term.js b/app/assets/javascripts/admin/controllers/admin-search-logs-term.js new file mode 100644 index 0000000000..cd493e35bc --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-search-logs-term.js @@ -0,0 +1,30 @@ +import I18n from "I18n"; +import Controller from "@ember/controller"; +import { DEFAULT_PERIOD } from "admin/controllers/admin-search-logs-index"; + +export default Controller.extend({ + loading: false, + term: null, + period: DEFAULT_PERIOD, + searchType: "all", + + init() { + this._super(...arguments); + + this.searchTypeOptions = [ + { + id: "all", + name: I18n.t("admin.logs.search_logs.types.all_search_types") + }, + { id: "header", name: I18n.t("admin.logs.search_logs.types.header") }, + { + id: "full_page", + name: I18n.t("admin.logs.search_logs.types.full_page") + }, + { + id: "click_through_only", + name: I18n.t("admin.logs.search_logs.types.click_through_only") + } + ]; + } +}); 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.es6 deleted file mode 100644 index 66def62c97..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 +++ /dev/null @@ -1,29 +0,0 @@ -import Controller from "@ember/controller"; -import { DEFAULT_PERIOD } from "admin/controllers/admin-search-logs-index"; - -export default Controller.extend({ - loading: false, - term: null, - period: DEFAULT_PERIOD, - searchType: "all", - - init() { - this._super(...arguments); - - this.searchTypeOptions = [ - { - id: "all", - name: I18n.t("admin.logs.search_logs.types.all_search_types") - }, - { id: "header", name: I18n.t("admin.logs.search_logs.types.header") }, - { - id: "full_page", - name: I18n.t("admin.logs.search_logs.types.full_page") - }, - { - id: "click_through_only", - name: I18n.t("admin.logs.search_logs.types.click_through_only") - } - ]; - } -}); 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..04826ca133 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js @@ -0,0 +1,121 @@ +import I18n from "I18n"; +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, pluginFilter; + if (this.filter) { + filter = this.filter + .toLowerCase() + .split(" ") + .filter(word => { + if (word.length === 0) { + return false; + } + + if (word.startsWith("plugin:")) { + pluginFilter = word.substr("plugin:".length).trim(); + return false; + } + + return true; + }) + .join(" ") + .trim(); + } + + if ( + (!filter || 0 === filter.length) && + (!pluginFilter || 0 === pluginFilter.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 (pluginFilter && item.plugin !== pluginFilter) 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 b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js new file mode 100644 index 0000000000..417c823824 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js @@ -0,0 +1,43 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import Controller from "@ember/controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; + +export default Controller.extend(bufferedProperty("siteText"), { + saved: false, + + @discourseComputed("buffered.value") + saveDisabled(value) { + return this.siteText.value === value; + }, + + actions: { + saveChanges() { + const buffered = this.buffered; + this.siteText + .save(buffered.getProperties("value")) + .then(() => { + this.commitBuffer(); + this.set("saved", true); + }) + .catch(popupAjaxError); + }, + + revertChanges() { + this.set("saved", false); + bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), result => { + if (result) { + this.siteText + .revert() + .then(props => { + const buffered = this.buffered; + buffered.setProperties(props); + this.commitBuffer(); + }) + .catch(popupAjaxError); + } + }); + } + } +}); 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.es6 deleted file mode 100644 index d24a172910..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 +++ /dev/null @@ -1,42 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; - -export default Controller.extend(bufferedProperty("siteText"), { - saved: false, - - @discourseComputed("buffered.value") - saveDisabled(value) { - return this.siteText.value === value; - }, - - actions: { - saveChanges() { - const buffered = this.buffered; - this.siteText - .save(buffered.getProperties("value")) - .then(() => { - this.commitBuffer(); - this.set("saved", true); - }) - .catch(popupAjaxError); - }, - - revertChanges() { - this.set("saved", false); - bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), result => { - if (result) { - this.siteText - .revert() - .then(props => { - const buffered = this.buffered; - buffered.setProperties(props); - this.commitBuffer(); - }) - .catch(popupAjaxError); - } - }); - } - } -}); 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..63e7edca25 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js @@ -0,0 +1,105 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/controllers/admin-user-fields.js new file mode 100644 index 0000000000..61e916748b --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-user-fields.js @@ -0,0 +1,71 @@ +import I18n from "I18n"; +import { gte, sort } from "@ember/object/computed"; +import Controller from "@ember/controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +const MAX_FIELDS = 20; + +export default Controller.extend({ + fieldTypes: null, + createDisabled: gte("model.length", MAX_FIELDS), + sortedFields: sort("model", "fieldSortOrder"), + + init() { + this._super(...arguments); + + this.fieldSortOrder = ["position"]; + }, + + actions: { + createField() { + const f = this.store.createRecord("user-field", { + field_type: "text", + position: MAX_FIELDS + }); + this.model.pushObject(f); + }, + + moveUp(f) { + const idx = this.sortedFields.indexOf(f); + if (idx) { + const prev = this.sortedFields.objectAt(idx - 1); + const prevPos = prev.get("position"); + + prev.update({ position: f.get("position") }); + f.update({ position: prevPos }); + } + }, + + moveDown(f) { + const idx = this.sortedFields.indexOf(f); + if (idx > -1) { + const next = this.sortedFields.objectAt(idx + 1); + const nextPos = next.get("position"); + + next.update({ position: f.get("position") }); + f.update({ position: nextPos }); + } + }, + + destroy(f) { + const model = this.model; + + // Only confirm if we already been saved + if (f.get("id")) { + bootbox.confirm(I18n.t("admin.user_fields.delete_confirm"), function( + result + ) { + if (result) { + f.destroyRecord() + .then(function() { + model.removeObject(f); + }) + .catch(popupAjaxError); + } + }); + } else { + model.removeObject(f); + } + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 deleted file mode 100644 index b81b08f559..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-user-fields.js.es6 +++ /dev/null @@ -1,70 +0,0 @@ -import { gte, sort } from "@ember/object/computed"; -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -const MAX_FIELDS = 20; - -export default Controller.extend({ - fieldTypes: null, - createDisabled: gte("model.length", MAX_FIELDS), - sortedFields: sort("model", "fieldSortOrder"), - - init() { - this._super(...arguments); - - this.fieldSortOrder = ["position"]; - }, - - actions: { - createField() { - const f = this.store.createRecord("user-field", { - field_type: "text", - position: MAX_FIELDS - }); - this.model.pushObject(f); - }, - - moveUp(f) { - const idx = this.sortedFields.indexOf(f); - if (idx) { - const prev = this.sortedFields.objectAt(idx - 1); - const prevPos = prev.get("position"); - - prev.update({ position: f.get("position") }); - f.update({ position: prevPos }); - } - }, - - moveDown(f) { - const idx = this.sortedFields.indexOf(f); - if (idx > -1) { - const next = this.sortedFields.objectAt(idx + 1); - const nextPos = next.get("position"); - - next.update({ position: f.get("position") }); - f.update({ position: nextPos }); - } - }, - - destroy(f) { - const model = this.model; - - // Only confirm if we already been saved - if (f.get("id")) { - bootbox.confirm(I18n.t("admin.user_fields.delete_confirm"), function( - result - ) { - if (result) { - f.destroyRecord() - .then(function() { - model.removeObject(f); - }) - .catch(popupAjaxError); - } - }); - } else { - model.removeObject(f); - } - } - } -}); 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..de6545056f --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js @@ -0,0 +1,324 @@ +import I18n from "I18n"; +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..9f82545dac --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-users-list-show.js @@ -0,0 +1,82 @@ +import I18n from "I18n"; +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, + asc: 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, + asc: this.asc, + 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..1a8070569f --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js @@ -0,0 +1,121 @@ +import I18n from "I18n"; +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..e6f20ee2f5 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-web-hooks-show.js @@ -0,0 +1,150 @@ +import I18n from "I18n"; +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"; +import { isAbsoluteURL } from "discourse-common/lib/get-url"; + +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+/) || + isAbsoluteURL(url) + ) { + 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 b/app/assets/javascripts/admin/controllers/admin-web-hooks.js new file mode 100644 index 0000000000..ec025662ef --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-web-hooks.js @@ -0,0 +1,29 @@ +import I18n from "I18n"; +import Controller from "@ember/controller"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default Controller.extend({ + actions: { + destroy(webhook) { + return bootbox.confirm( + I18n.t("admin.web_hooks.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + webhook + .destroyRecord() + .then(() => { + this.model.removeObject(webhook); + }) + .catch(popupAjaxError); + } + } + ); + }, + + loadMore() { + this.model.loadMore(); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-web-hooks.js.es6 b/app/assets/javascripts/admin/controllers/admin-web-hooks.js.es6 deleted file mode 100644 index f9f401e330..0000000000 --- a/app/assets/javascripts/admin/controllers/admin-web-hooks.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Controller.extend({ - actions: { - destroy(webhook) { - return bootbox.confirm( - I18n.t("admin.web_hooks.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - webhook - .destroyRecord() - .then(() => { - this.model.removeObject(webhook); - }) - .catch(popupAjaxError); - } - } - ); - }, - - loadMore() { - this.model.loadMore(); - } - } -}); 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..da260b409e --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js @@ -0,0 +1,142 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js new file mode 100644 index 0000000000..531d3ea665 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js @@ -0,0 +1,59 @@ +import I18n from "I18n"; +import { alias, map } from "@ember/object/computed"; +import Controller from "@ember/controller"; +import discourseComputed from "discourse-common/utils/decorators"; +import { escapeExpression } from "discourse/lib/utilities"; + +export default Controller.extend({ + sample: alias("model.sample"), + errors: alias("model.errors"), + count: alias("model.grant_count"), + + @discourseComputed("count", "sample.length") + countWarning(count, sampleLength) { + if (count <= 10) { + return sampleLength !== count; + } else { + return sampleLength !== 10; + } + }, + + @discourseComputed("model.query_plan") + hasQueryPlan(queryPlan) { + return !!queryPlan; + }, + + @discourseComputed("model.query_plan") + queryPlanHtml(queryPlan) { + let output = `
`;
+
+    queryPlan.forEach(linehash => {
+      output += escapeExpression(linehash["QUERY PLAN"]);
+      output += "
"; + }); + + output += "
"; + return output; + }, + + processedSample: map("model.sample", grant => { + let i18nKey = "admin.badges.preview.grant.with"; + const i18nParams = { username: escapeExpression(grant.username) }; + + if (grant.post_id) { + i18nKey += "_post"; + i18nParams.link = ` + ${escapeExpression(grant.title)} + `; + } + + if (grant.granted_at) { + i18nKey += "_time"; + i18nParams.time = escapeExpression( + moment(grant.granted_at).format(I18n.t("dates.long_with_year")) + ); + } + + return I18n.t(i18nKey, i18nParams); + }) +}); 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.es6 deleted file mode 100644 index 84325f90d1..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 +++ /dev/null @@ -1,58 +0,0 @@ -import { alias, map } from "@ember/object/computed"; -import Controller from "@ember/controller"; -import discourseComputed from "discourse-common/utils/decorators"; -import { escapeExpression } from "discourse/lib/utilities"; - -export default Controller.extend({ - sample: alias("model.sample"), - errors: alias("model.errors"), - count: alias("model.grant_count"), - - @discourseComputed("count", "sample.length") - countWarning(count, sampleLength) { - if (count <= 10) { - return sampleLength !== count; - } else { - return sampleLength !== 10; - } - }, - - @discourseComputed("model.query_plan") - hasQueryPlan(queryPlan) { - return !!queryPlan; - }, - - @discourseComputed("model.query_plan") - queryPlanHtml(queryPlan) { - let output = `
`;
-
-    queryPlan.forEach(linehash => {
-      output += escapeExpression(linehash["QUERY PLAN"]);
-      output += "
"; - }); - - output += "
"; - return output; - }, - - processedSample: map("model.sample", grant => { - let i18nKey = "admin.badges.preview.grant.with"; - const i18nParams = { username: escapeExpression(grant.username) }; - - if (grant.post_id) { - i18nKey += "_post"; - i18nParams.link = ` - ${Handlebars.Utils.escapeExpression(grant.title)} - `; - } - - if (grant.granted_at) { - i18nKey += "_time"; - i18nParams.time = escapeExpression( - moment(grant.granted_at).format(I18n.t("dates.long_with_year")) - ); - } - - return I18n.t(i18nKey, i18nParams); - }) -}); 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..1372f16ca9 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js @@ -0,0 +1,84 @@ +import I18n from "I18n"; +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..7b74bc9964 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-install-theme.js @@ -0,0 +1,199 @@ +import I18n from "I18n"; +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..08ac66070c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js @@ -0,0 +1,40 @@ +import I18n from "I18n"; +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; +import { action } from "@ember/object"; + +export default Controller.extend(ModalFunctionality, { + adminUserIndex: controller(), + username: alias("model.username"), + targetUsername: alias("model.targetUsername"), + + onShow() { + this.set("value", null); + }, + + @discourseComputed("username", "targetUsername") + text(username, targetUsername) { + return I18n.t(`admin.user.merge.confirmation.text`, { + username, + targetUsername + }); + }, + + @discourseComputed("value", "text") + mergeDisabled(value, text) { + return !value || text !== value; + }, + + @action + confirm() { + this.adminUserIndex.send("merge", this.targetUsername); + this.send("closeModal"); + }, + + @action + close() { + 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..28fd6058e4 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js @@ -0,0 +1,30 @@ +import Controller, { inject as controller } from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import discourseComputed from "discourse-common/utils/decorators"; +import { alias } from "@ember/object/computed"; +import { action } from "@ember/object"; + +export default Controller.extend(ModalFunctionality, { + adminUserIndex: controller(), + username: alias("model.username"), + + onShow() { + this.set("targetUsername", null); + }, + + @discourseComputed("username", "targetUsername") + mergeDisabled(username, targetUsername) { + return !targetUsername || username === targetUsername; + }, + + @action + showConfirmation() { + this.send("closeModal"); + this.adminUserIndex.send("showMergeConfirmation", this.targetUsername); + }, + + @action + close() { + 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..44bf29a207 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-reseed.js @@ -0,0 +1,44 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/helpers/check-icon.js new file mode 100644 index 0000000000..4641a15fa2 --- /dev/null +++ b/app/assets/javascripts/admin/helpers/check-icon.js @@ -0,0 +1,8 @@ +import { registerUnbound } from "discourse-common/lib/helpers"; +import { renderIcon } from "discourse-common/lib/icon-library"; +import { htmlSafe } from "@ember/template"; + +registerUnbound("check-icon", function(value) { + let icon = value ? "check" : "times"; + return htmlSafe(renderIcon("string", icon)); +}); diff --git a/app/assets/javascripts/admin/helpers/check-icon.js.es6 b/app/assets/javascripts/admin/helpers/check-icon.js.es6 deleted file mode 100644 index 88cddc3d4d..0000000000 --- a/app/assets/javascripts/admin/helpers/check-icon.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -import { registerUnbound } from "discourse-common/lib/helpers"; -import { renderIcon } from "discourse-common/lib/icon-library"; - -registerUnbound("check-icon", function(value) { - let icon = value ? "check" : "times"; - return new Handlebars.SafeString(renderIcon("string", icon)); -}); 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 b/app/assets/javascripts/admin/helpers/human-size.js new file mode 100644 index 0000000000..fe543937f1 --- /dev/null +++ b/app/assets/javascripts/admin/helpers/human-size.js @@ -0,0 +1,4 @@ +import I18n from "I18n"; +import { htmlHelper } from "discourse-common/lib/helpers"; + +export default htmlHelper(size => I18n.toHumanSize(size)); diff --git a/app/assets/javascripts/admin/helpers/human-size.js.es6 b/app/assets/javascripts/admin/helpers/human-size.js.es6 deleted file mode 100644 index cb022e64e1..0000000000 --- a/app/assets/javascripts/admin/helpers/human-size.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import { htmlHelper } from "discourse-common/lib/helpers"; - -export default htmlHelper(size => I18n.toHumanSize(size)); diff --git a/app/assets/javascripts/admin/helpers/post-action-title.js b/app/assets/javascripts/admin/helpers/post-action-title.js new file mode 100644 index 0000000000..53b5922ae5 --- /dev/null +++ b/app/assets/javascripts/admin/helpers/post-action-title.js @@ -0,0 +1,17 @@ +import I18n from "I18n"; +import Helper from "@ember/component/helper"; + +function postActionTitle([id, nameKey]) { + let title = I18n.t(`admin.flags.short_names.${nameKey}`, { + defaultValue: null + }); + + // TODO: We can remove this once other translations have been updated + if (!title) { + return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 }); + } + + return title; +} + +export default Helper.helper(postActionTitle); diff --git a/app/assets/javascripts/admin/helpers/post-action-title.js.es6 b/app/assets/javascripts/admin/helpers/post-action-title.js.es6 deleted file mode 100644 index 657aee2e9b..0000000000 --- a/app/assets/javascripts/admin/helpers/post-action-title.js.es6 +++ /dev/null @@ -1,16 +0,0 @@ -import Helper from "@ember/component/helper"; - -function postActionTitle([id, nameKey]) { - let title = I18n.t(`admin.flags.short_names.${nameKey}`, { - defaultValue: null - }); - - // TODO: We can remove this once other translations have been updated - if (!title) { - return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 }); - } - - return title; -} - -export default Helper.helper(postActionTitle); 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..73349e1067 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/penalty-controller.js @@ -0,0 +1,70 @@ +import I18n from "I18n"; +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 46ba124e18..0000000000 --- a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -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/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..699cecd32e --- /dev/null +++ b/app/assets/javascripts/admin/mixins/setting-component.js @@ -0,0 +1,257 @@ +import I18n from "I18n"; +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"; +import { htmlSafe } from "@ember/template"; + +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", + "simple_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 htmlSafe( + "
" + + 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 b/app/assets/javascripts/admin/mixins/setting-object.js new file mode 100644 index 0000000000..bfacbedc8e --- /dev/null +++ b/app/assets/javascripts/admin/mixins/setting-object.js @@ -0,0 +1,65 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { computed } from "@ember/object"; +import Mixin from "@ember/object/mixin"; +import { isPresent } from "@ember/utils"; + +export default Mixin.create({ + @discourseComputed("value", "default") + overridden(val, defaultVal) { + if (val === null) val = ""; + if (defaultVal === null) defaultVal = ""; + + return val.toString() !== defaultVal.toString(); + }, + + computedValueProperty: computed( + "valueProperty", + "validValues.[]", + function() { + if (isPresent(this.valueProperty)) { + return this.valueProperty; + } + + if (isPresent(this.validValues.get("firstObject.value"))) { + return "value"; + } else { + return null; + } + } + ), + + computedNameProperty: computed("nameProperty", "validValues.[]", function() { + if (isPresent(this.nameProperty)) { + return this.nameProperty; + } + + if (isPresent(this.validValues.get("firstObject.name"))) { + return "name"; + } else { + return null; + } + }), + + @discourseComputed("valid_values") + validValues(validValues) { + const vals = [], + translateNames = this.translate_names; + + validValues.forEach(v => { + if (v.name && v.name.length > 0 && translateNames) { + vals.addObject({ name: I18n.t(v.name), value: v.value }); + } else { + vals.addObject(v); + } + }); + return vals; + }, + + @discourseComputed("valid_values") + allowsNone(validValues) { + if (validValues && validValues.indexOf("") >= 0) { + return "admin.settings.none"; + } + } +}); diff --git a/app/assets/javascripts/admin/mixins/setting-object.js.es6 b/app/assets/javascripts/admin/mixins/setting-object.js.es6 deleted file mode 100644 index 0d97e79873..0000000000 --- a/app/assets/javascripts/admin/mixins/setting-object.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { computed } from "@ember/object"; -import Mixin from "@ember/object/mixin"; -import { isPresent } from "@ember/utils"; - -export default Mixin.create({ - @discourseComputed("value", "default") - overridden(val, defaultVal) { - if (val === null) val = ""; - if (defaultVal === null) defaultVal = ""; - - return val.toString() !== defaultVal.toString(); - }, - - computedValueProperty: computed( - "valueProperty", - "validValues.[]", - function() { - if (isPresent(this.valueProperty)) { - return this.valueProperty; - } - - if (isPresent(this.validValues.get("firstObject.value"))) { - return "value"; - } else { - return null; - } - } - ), - - computedNameProperty: computed("nameProperty", "validValues.[]", function() { - if (isPresent(this.nameProperty)) { - return this.nameProperty; - } - - if (isPresent(this.validValues.get("firstObject.name"))) { - return "name"; - } else { - return null; - } - }), - - @discourseComputed("valid_values") - validValues(validValues) { - const vals = [], - translateNames = this.translate_names; - - validValues.forEach(v => { - if (v.name && v.name.length > 0 && translateNames) { - vals.addObject({ name: I18n.t(v.name), value: v.value }); - } else { - vals.addObject(v); - } - }); - return vals; - }, - - @discourseComputed("valid_values") - allowsNone(validValues) { - if (validValues && validValues.indexOf("") >= 0) { - return "admin.settings.none"; - } - } -}); 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..8dc7685cc1 --- /dev/null +++ b/app/assets/javascripts/admin/models/admin-user.js @@ -0,0 +1,586 @@ +import getURL from "discourse-common/lib/get-url"; +import I18n from "I18n"; +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 DiscourseURL, { 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 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); + } + }); + }, + + 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 = 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 = getURL( + `/admin/users/${user.get("id")}/${data.username}` + ); + } else { + document.location = 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 = 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.id}/merge.json`, { + type: "POST", + data: formData + }) + .then(data => { + if (data.merged) { + if (/^\/admin\/users\/list\//.test(location)) { + DiscourseURL.redirectTo(location); + } else { + DiscourseURL.redirectTo( + `/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(() => { + AdminUser.find(user.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 b/app/assets/javascripts/admin/models/backup.js new file mode 100644 index 0000000000..0b6f7122a4 --- /dev/null +++ b/app/assets/javascripts/admin/models/backup.js @@ -0,0 +1,76 @@ +import getURL from "discourse-common/lib/get-url"; +import I18n from "I18n"; +import { ajax } from "discourse/lib/ajax"; +import { extractError } from "discourse/lib/ajax-error"; +import EmberObject from "@ember/object"; +import MessageBus from "message-bus-client"; + +const Backup = EmberObject.extend({ + destroy() { + return ajax("/admin/backups/" + this.filename, { type: "DELETE" }); + }, + + restore() { + return ajax("/admin/backups/" + this.filename + "/restore", { + type: "POST", + data: { client_id: MessageBus.clientId } + }); + } +}); + +Backup.reopenClass({ + find() { + return ajax("/admin/backups.json") + .then(backups => backups.map(backup => Backup.create(backup))) + .catch(error => { + bootbox.alert( + I18n.t("admin.backups.backup_storage_error", { + error_message: extractError(error) + }) + ); + return []; + }); + }, + + start(withUploads) { + if (withUploads === undefined) { + withUploads = true; + } + return ajax("/admin/backups", { + type: "POST", + data: { + with_uploads: withUploads, + client_id: MessageBus.clientId + } + }).then(result => { + if (!result.success) { + bootbox.alert(result.message); + } + }); + }, + + cancel() { + return ajax("/admin/backups/cancel.json", { + type: "DELETE" + }).then(result => { + if (!result.success) { + bootbox.alert(result.message); + } + }); + }, + + rollback() { + return ajax("/admin/backups/rollback.json", { + type: "POST" + }).then(result => { + if (!result.success) { + bootbox.alert(result.message); + } else { + // redirect to homepage (session might be lost) + window.location = getURL("/"); + } + }); + } +}); + +export default Backup; diff --git a/app/assets/javascripts/admin/models/backup.js.es6 b/app/assets/javascripts/admin/models/backup.js.es6 deleted file mode 100644 index 882173300a..0000000000 --- a/app/assets/javascripts/admin/models/backup.js.es6 +++ /dev/null @@ -1,73 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import { extractError } from "discourse/lib/ajax-error"; -import EmberObject from "@ember/object"; - -const Backup = EmberObject.extend({ - destroy() { - return ajax("/admin/backups/" + this.filename, { type: "DELETE" }); - }, - - restore() { - return ajax("/admin/backups/" + this.filename + "/restore", { - type: "POST", - data: { client_id: window.MessageBus.clientId } - }); - } -}); - -Backup.reopenClass({ - find() { - return ajax("/admin/backups.json") - .then(backups => backups.map(backup => Backup.create(backup))) - .catch(error => { - bootbox.alert( - I18n.t("admin.backups.backup_storage_error", { - error_message: extractError(error) - }) - ); - return []; - }); - }, - - start(withUploads) { - if (withUploads === undefined) { - withUploads = true; - } - return ajax("/admin/backups", { - type: "POST", - data: { - with_uploads: withUploads, - client_id: window.MessageBus.clientId - } - }).then(result => { - if (!result.success) { - bootbox.alert(result.message); - } - }); - }, - - cancel() { - return ajax("/admin/backups/cancel.json", { - type: "DELETE" - }).then(result => { - if (!result.success) { - bootbox.alert(result.message); - } - }); - }, - - rollback() { - return ajax("/admin/backups/rollback.json", { - type: "POST" - }).then(result => { - if (!result.success) { - bootbox.alert(result.message); - } else { - // redirect to homepage (session might be lost) - window.location = Discourse.getURL("/"); - } - }); - } -}); - -export default Backup; diff --git a/app/assets/javascripts/admin/models/color-scheme-color.js b/app/assets/javascripts/admin/models/color-scheme-color.js new file mode 100644 index 0000000000..e55fa258f8 --- /dev/null +++ b/app/assets/javascripts/admin/models/color-scheme-color.js @@ -0,0 +1,104 @@ +import I18n from "I18n"; +import discourseComputed, { + observes, + on +} from "discourse-common/utils/decorators"; +import { propertyNotEqual } from "discourse/lib/computed"; +import EmberObject from "@ember/object"; + +const ColorSchemeColor = EmberObject.extend({ + @on("init") + startTrackingChanges() { + this.set("originals", { hex: this.hex || "FFFFFF" }); + + // force changed property to be recalculated + this.notifyPropertyChange("hex"); + }, + + // Whether value has changed since it was last saved. + @discourseComputed("hex") + changed(hex) { + if (!this.originals) return false; + if (hex !== this.originals.hex) return true; + + return false; + }, + + // Whether the current value is different than Discourse's default color scheme. + overridden: propertyNotEqual("hex", "default_hex"), + + // Whether the saved value is different than Discourse's default color scheme. + @discourseComputed("default_hex", "hex") + savedIsOverriden(defaultHex) { + return this.originals.hex !== defaultHex; + }, + + revert() { + this.set("hex", this.default_hex); + }, + + undo() { + if (this.originals) { + this.set("hex", this.originals.hex); + } + }, + + @discourseComputed("name") + translatedName(name) { + if (!this.is_advanced) { + return I18n.t(`admin.customize.colors.${name}.name`); + } else { + return name; + } + }, + + @discourseComputed("name") + description(name) { + if (!this.is_advanced) { + return I18n.t(`admin.customize.colors.${name}.description`); + } else { + return ""; + } + }, + + /** + brightness returns a number between 0 (darkest) to 255 (brightest). + Undefined if hex is not a valid color. + + @property brightness + **/ + @discourseComputed("hex") + brightness(hex) { + if (hex.length === 6 || hex.length === 3) { + if (hex.length === 3) { + hex = + hex.substr(0, 1) + + hex.substr(0, 1) + + hex.substr(1, 1) + + hex.substr(1, 1) + + hex.substr(2, 1) + + hex.substr(2, 1); + } + return Math.round( + (parseInt(hex.substr(0, 2), 16) * 299 + + parseInt(hex.substr(2, 2), 16) * 587 + + parseInt(hex.substr(4, 2), 16) * 114) / + 1000 + ); + } + }, + + @observes("hex") + hexValueChanged() { + if (this.hex) { + this.set("hex", this.hex.toString().replace(/[^0-9a-fA-F]/g, "")); + } + }, + + @discourseComputed("hex") + valid(hex) { + return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null; + } +}); + +export default ColorSchemeColor; diff --git a/app/assets/javascripts/admin/models/color-scheme-color.js.es6 b/app/assets/javascripts/admin/models/color-scheme-color.js.es6 deleted file mode 100644 index f1aa235a4b..0000000000 --- a/app/assets/javascripts/admin/models/color-scheme-color.js.es6 +++ /dev/null @@ -1,103 +0,0 @@ -import discourseComputed, { - observes, - on -} from "discourse-common/utils/decorators"; -import { propertyNotEqual } from "discourse/lib/computed"; -import EmberObject from "@ember/object"; - -const ColorSchemeColor = EmberObject.extend({ - @on("init") - startTrackingChanges() { - this.set("originals", { hex: this.hex || "FFFFFF" }); - - // force changed property to be recalculated - this.notifyPropertyChange("hex"); - }, - - // Whether value has changed since it was last saved. - @discourseComputed("hex") - changed(hex) { - if (!this.originals) return false; - if (hex !== this.originals.hex) return true; - - return false; - }, - - // Whether the current value is different than Discourse's default color scheme. - overridden: propertyNotEqual("hex", "default_hex"), - - // Whether the saved value is different than Discourse's default color scheme. - @discourseComputed("default_hex", "hex") - savedIsOverriden(defaultHex) { - return this.originals.hex !== defaultHex; - }, - - revert() { - this.set("hex", this.default_hex); - }, - - undo() { - if (this.originals) { - this.set("hex", this.originals.hex); - } - }, - - @discourseComputed("name") - translatedName(name) { - if (!this.is_advanced) { - return I18n.t(`admin.customize.colors.${name}.name`); - } else { - return name; - } - }, - - @discourseComputed("name") - description(name) { - if (!this.is_advanced) { - return I18n.t(`admin.customize.colors.${name}.description`); - } else { - return ""; - } - }, - - /** - brightness returns a number between 0 (darkest) to 255 (brightest). - Undefined if hex is not a valid color. - - @property brightness - **/ - @discourseComputed("hex") - brightness(hex) { - if (hex.length === 6 || hex.length === 3) { - if (hex.length === 3) { - hex = - hex.substr(0, 1) + - hex.substr(0, 1) + - hex.substr(1, 1) + - hex.substr(1, 1) + - hex.substr(2, 1) + - hex.substr(2, 1); - } - return Math.round( - (parseInt(hex.substr(0, 2), 16) * 299 + - parseInt(hex.substr(2, 2), 16) * 587 + - parseInt(hex.substr(4, 2), 16) * 114) / - 1000 - ); - } - }, - - @observes("hex") - hexValueChanged() { - if (this.hex) { - this.set("hex", this.hex.toString().replace(/[^0-9a-fA-F]/g, "")); - } - }, - - @discourseComputed("hex") - valid(hex) { - return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null; - } -}); - -export default ColorSchemeColor; 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..5fe6337f0d --- /dev/null +++ b/app/assets/javascripts/admin/models/color-scheme.js @@ -0,0 +1,148 @@ +import I18n from "I18n"; +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 8486002386..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(Ember.Copyable, { - 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 b/app/assets/javascripts/admin/models/email-log.js new file mode 100644 index 0000000000..1317df2943 --- /dev/null +++ b/app/assets/javascripts/admin/models/email-log.js @@ -0,0 +1,36 @@ +import getURL from "discourse-common/lib/get-url"; +import { ajax } from "discourse/lib/ajax"; +import AdminUser from "admin/models/admin-user"; +import EmberObject from "@ember/object"; + +const EmailLog = EmberObject.extend({}); + +EmailLog.reopenClass({ + create(attrs) { + attrs = attrs || {}; + + if (attrs.user) { + attrs.user = AdminUser.create(attrs.user); + } + + if (attrs.post_url) { + attrs.post_url = getURL(attrs.post_url); + } + + return this._super(attrs); + }, + + findAll(filter, offset) { + filter = filter || {}; + offset = offset || 0; + + const status = filter.status || "sent"; + filter = _.omit(filter, "status"); + + return ajax(`/admin/email/${status}.json?offset=${offset}`, { + data: filter + }).then(logs => logs.map(log => EmailLog.create(log))); + } +}); + +export default EmailLog; diff --git a/app/assets/javascripts/admin/models/email-log.js.es6 b/app/assets/javascripts/admin/models/email-log.js.es6 deleted file mode 100644 index c2eaaa26e6..0000000000 --- a/app/assets/javascripts/admin/models/email-log.js.es6 +++ /dev/null @@ -1,35 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import AdminUser from "admin/models/admin-user"; -import EmberObject from "@ember/object"; - -const EmailLog = EmberObject.extend({}); - -EmailLog.reopenClass({ - create(attrs) { - attrs = attrs || {}; - - if (attrs.user) { - attrs.user = AdminUser.create(attrs.user); - } - - if (attrs.post_url) { - attrs.post_url = Discourse.getURL(attrs.post_url); - } - - return this._super(attrs); - }, - - findAll(filter, offset) { - filter = filter || {}; - offset = offset || 0; - - const status = filter.status || "sent"; - filter = _.omit(filter, "status"); - - return ajax(`/admin/email/${status}.json?offset=${offset}`, { - data: filter - }).then(logs => logs.map(log => EmailLog.create(log))); - } -}); - -export default EmailLog; 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 b/app/assets/javascripts/admin/models/flag-type.js new file mode 100644 index 0000000000..1079110045 --- /dev/null +++ b/app/assets/javascripts/admin/models/flag-type.js @@ -0,0 +1,10 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import RestModel from "discourse/models/rest"; + +export default RestModel.extend({ + @discourseComputed("id") + name(id) { + return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 }); + } +}); diff --git a/app/assets/javascripts/admin/models/flag-type.js.es6 b/app/assets/javascripts/admin/models/flag-type.js.es6 deleted file mode 100644 index 93fb2eacc9..0000000000 --- a/app/assets/javascripts/admin/models/flag-type.js.es6 +++ /dev/null @@ -1,9 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import RestModel from "discourse/models/rest"; - -export default RestModel.extend({ - @discourseComputed("id") - name(id) { - return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 }); - } -}); 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 b/app/assets/javascripts/admin/models/report.js new file mode 100644 index 0000000000..9d2f25f9a1 --- /dev/null +++ b/app/assets/javascripts/admin/models/report.js @@ -0,0 +1,560 @@ +import getURL from "discourse-common/lib/get-url"; +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { makeArray } from "discourse-common/lib/helpers"; +import { isEmpty } from "@ember/utils"; +import EmberObject from "@ember/object"; +import { escapeExpression } from "discourse/lib/utilities"; +import { ajax } from "discourse/lib/ajax"; +import round from "discourse/lib/round"; +import { + fillMissingDates, + formatUsername, + toNumber +} from "discourse/lib/utilities"; +import { number, durationTiny } from "discourse/lib/formatter"; +import { renderAvatar } from "discourse/helpers/user-avatar"; + +// Change this line each time report format change +// and you want to ensure cache is reset +export const SCHEMA_VERSION = 4; + +const Report = EmberObject.extend({ + average: false, + percent: false, + higher_is_better: true, + description_link: null, + description: null, + + @discourseComputed("type", "start_date", "end_date") + reportUrl(type, start_date, end_date) { + start_date = moment + .utc(start_date) + .locale("en") + .format("YYYY-MM-DD"); + + end_date = moment + .utc(end_date) + .locale("en") + .format("YYYY-MM-DD"); + + return getURL( + `/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}` + ); + }, + + valueAt(numDaysAgo) { + if (this.data) { + const wantedDate = moment() + .subtract(numDaysAgo, "days") + .locale("en") + .format("YYYY-MM-DD"); + const item = this.data.find(d => d.x === wantedDate); + if (item) { + return item.y; + } + } + return 0; + }, + + valueFor(startDaysAgo, endDaysAgo) { + if (this.data) { + const earliestDate = moment() + .subtract(endDaysAgo, "days") + .startOf("day"); + const latestDate = moment() + .subtract(startDaysAgo, "days") + .startOf("day"); + let d, + sum = 0, + count = 0; + this.data.forEach(datum => { + d = moment(datum.x); + if (d >= earliestDate && d <= latestDate) { + sum += datum.y; + count++; + } + }); + if (this.method === "average" && count > 0) { + sum /= count; + } + return round(sum, -2); + } + }, + + @discourseComputed("data", "average") + todayCount() { + return this.valueAt(0); + }, + + @discourseComputed("data", "average") + yesterdayCount() { + return this.valueAt(1); + }, + + @discourseComputed("data", "average") + sevenDaysAgoCount() { + return this.valueAt(7); + }, + + @discourseComputed("data", "average") + thirtyDaysAgoCount() { + return this.valueAt(30); + }, + + @discourseComputed("data", "average") + lastSevenDaysCount() { + return this.averageCount(7, this.valueFor(1, 7)); + }, + + @discourseComputed("data", "average") + lastThirtyDaysCount() { + return this.averageCount(30, this.valueFor(1, 30)); + }, + + averageCount(count, value) { + return this.average ? value / count : value; + }, + + @discourseComputed("yesterdayCount", "higher_is_better") + yesterdayTrend(yesterdayCount, higherIsBetter) { + return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter); + }, + + @discourseComputed("lastSevenDaysCount", "higher_is_better") + sevenDaysTrend(lastSevenDaysCount, higherIsBetter) { + return this._computeTrend( + this.valueFor(8, 14), + lastSevenDaysCount, + higherIsBetter + ); + }, + + @discourseComputed("data") + currentTotal(data) { + return data.reduce((cur, pair) => cur + pair.y, 0); + }, + + @discourseComputed("data", "currentTotal") + currentAverage(data, total) { + return makeArray(data).length === 0 + ? 0 + : parseFloat((total / parseFloat(data.length)).toFixed(1)); + }, + + @discourseComputed("trend", "higher_is_better") + trendIcon(trend, higherIsBetter) { + return this._iconForTrend(trend, higherIsBetter); + }, + + @discourseComputed("sevenDaysTrend", "higher_is_better") + sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) { + return this._iconForTrend(sevenDaysTrend, higherIsBetter); + }, + + @discourseComputed("thirtyDaysTrend", "higher_is_better") + thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) { + return this._iconForTrend(thirtyDaysTrend, higherIsBetter); + }, + + @discourseComputed("yesterdayTrend", "higher_is_better") + yesterdayTrendIcon(yesterdayTrend, higherIsBetter) { + return this._iconForTrend(yesterdayTrend, higherIsBetter); + }, + + @discourseComputed( + "prev_period", + "currentTotal", + "currentAverage", + "higher_is_better" + ) + trend(prev, currentTotal, currentAverage, higherIsBetter) { + const total = this.average ? currentAverage : currentTotal; + return this._computeTrend(prev, total, higherIsBetter); + }, + + @discourseComputed("prev30Days", "lastThirtyDaysCount", "higher_is_better") + thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) { + return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter); + }, + + @discourseComputed("type") + method(type) { + if (type === "time_to_first_response") { + return "average"; + } else { + return "sum"; + } + }, + + percentChangeString(val1, val2) { + const change = this._computeChange(val1, val2); + + if (isNaN(change) || !isFinite(change)) { + return null; + } else if (change > 0) { + return "+" + change.toFixed(0) + "%"; + } else { + return change.toFixed(0) + "%"; + } + }, + + @discourseComputed("prev_period", "currentTotal", "currentAverage") + trendTitle(prev, currentTotal, currentAverage) { + let current = this.average ? currentAverage : currentTotal; + let percent = this.percentChangeString(prev, current); + + if (this.average) { + prev = prev ? prev.toFixed(1) : "0"; + if (this.percent) { + current += "%"; + prev += "%"; + } + } else { + prev = number(prev); + current = number(current); + } + + return I18n.t("admin.dashboard.reports.trend_title", { + percent, + prev, + current + }); + }, + + changeTitle(valAtT1, valAtT2, prevPeriodString) { + const change = this.percentChangeString(valAtT1, valAtT2); + let title = ""; + if (change) { + title += `${change} change. `; + } + title += `Was ${number(valAtT1)} ${prevPeriodString}.`; + return title; + }, + + @discourseComputed("yesterdayCount") + yesterdayCountTitle(yesterdayCount) { + return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago"); + }, + + @discourseComputed("lastSevenDaysCount") + sevenDaysCountTitle(lastSevenDaysCount) { + return this.changeTitle( + this.valueFor(8, 14), + lastSevenDaysCount, + "two weeks ago" + ); + }, + + @discourseComputed("prev30Days", "lastThirtyDaysCount") + thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) { + return this.changeTitle( + prev30Days, + lastThirtyDaysCount, + "in the previous 30 day period" + ); + }, + + @discourseComputed("data") + sortedData(data) { + return this.xAxisIsDate ? data.toArray().reverse() : data.toArray(); + }, + + @discourseComputed("data") + xAxisIsDate() { + if (!this.data[0]) return false; + return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); + }, + + @discourseComputed("labels") + computedLabels(labels) { + return labels.map(label => { + const type = label.type || "string"; + + let mainProperty; + if (label.property) mainProperty = label.property; + else if (type === "user") mainProperty = label.properties["username"]; + else if (type === "topic") mainProperty = label.properties["title"]; + else if (type === "post") + mainProperty = label.properties["truncated_raw"]; + else mainProperty = label.properties[0]; + + return { + title: label.title, + sortProperty: label.sort_property || mainProperty, + mainProperty, + type, + compute: (row, opts = {}) => { + let value = null; + + if (opts.useSortProperty) { + value = row[label.sort_property || mainProperty]; + } else { + value = row[mainProperty]; + } + + if (type === "user") return this._userLabel(label.properties, row); + if (type === "post") return this._postLabel(label.properties, row); + if (type === "topic") return this._topicLabel(label.properties, row); + if (type === "seconds") return this._secondsLabel(value); + if (type === "link") return this._linkLabel(label.properties, row); + if (type === "percent") return this._percentLabel(value); + if (type === "bytes") return this._bytesLabel(value); + if (type === "number") { + return this._numberLabel(value, opts); + } + if (type === "date") { + const date = moment(value); + if (date.isValid()) return this._dateLabel(value, date); + } + if (type === "precise_date") { + const date = moment(value); + if (date.isValid()) return this._dateLabel(value, date, "LLL"); + } + if (type === "text") return this._textLabel(value); + + return { + value, + type, + property: mainProperty, + formatedValue: value ? escapeExpression(value) : "—" + }; + } + }; + }); + }, + + _userLabel(properties, row) { + const username = row[properties.username]; + + const formatedValue = () => { + const userId = row[properties.id]; + + const user = EmberObject.create({ + username, + name: formatUsername(username), + avatar_template: row[properties.avatar] + }); + + const href = getURL(`/admin/users/${userId}/${username}`); + + const avatarImg = renderAvatar(user, { + imageSize: "tiny", + ignoreTitle: true + }); + + return `${avatarImg}${user.name}`; + }; + + return { + value: username, + formatedValue: username ? formatedValue() : "—" + }; + }, + + _topicLabel(properties, row) { + const topicTitle = row[properties.title]; + + const formatedValue = () => { + const topicId = row[properties.id]; + const href = getURL(`/t/-/${topicId}`); + return `${escapeExpression(topicTitle)}`; + }; + + return { + value: topicTitle, + formatedValue: topicTitle ? formatedValue() : "—" + }; + }, + + _postLabel(properties, row) { + const postTitle = row[properties.truncated_raw]; + const postNumber = row[properties.number]; + const topicId = row[properties.topic_id]; + const href = getURL(`/t/-/${topicId}/${postNumber}`); + + return { + property: properties.title, + value: postTitle, + formatedValue: + postTitle && href + ? `${escapeExpression(postTitle)}` + : "—" + }; + }, + + _secondsLabel(value) { + return { + value: toNumber(value), + formatedValue: durationTiny(value) + }; + }, + + _percentLabel(value) { + return { + value: toNumber(value), + formatedValue: value ? `${value}%` : "—" + }; + }, + + _numberLabel(value, options = {}) { + const formatNumbers = isEmpty(options.formatNumbers) + ? true + : options.formatNumbers; + + const formatedValue = () => (formatNumbers ? number(value) : value); + + return { + value: toNumber(value), + formatedValue: value ? formatedValue() : "—" + }; + }, + + _bytesLabel(value) { + return { + value: toNumber(value), + formatedValue: I18n.toHumanSize(value) + }; + }, + + _dateLabel(value, date, format = "LL") { + return { + value, + formatedValue: value ? date.format(format) : "—" + }; + }, + + _textLabel(value) { + const escaped = escapeExpression(value); + + return { + value, + formatedValue: value ? escaped : "—" + }; + }, + + _linkLabel(properties, row) { + const property = properties[0]; + const value = getURL(row[property]); + const formatedValue = (href, anchor) => { + return `${escapeExpression( + anchor + )}`; + }; + + return { + value, + formatedValue: value ? formatedValue(value, row[properties[1]]) : "—" + }; + }, + + _computeChange(valAtT1, valAtT2) { + return ((valAtT2 - valAtT1) / valAtT1) * 100; + }, + + _computeTrend(valAtT1, valAtT2, higherIsBetter) { + const change = this._computeChange(valAtT1, valAtT2); + + if (change > 50) { + return higherIsBetter ? "high-trending-up" : "high-trending-down"; + } else if (change > 2) { + return higherIsBetter ? "trending-up" : "trending-down"; + } else if (change <= 2 && change >= -2) { + return "no-change"; + } else if (change < -50) { + return higherIsBetter ? "high-trending-down" : "high-trending-up"; + } else if (change < -2) { + return higherIsBetter ? "trending-down" : "trending-up"; + } + }, + + _iconForTrend(trend, higherIsBetter) { + switch (trend) { + case "trending-up": + return higherIsBetter ? "angle-up" : "angle-down"; + case "trending-down": + return higherIsBetter ? "angle-down" : "angle-up"; + case "high-trending-up": + return higherIsBetter ? "angle-double-up" : "angle-double-down"; + case "high-trending-down": + return higherIsBetter ? "angle-double-down" : "angle-double-up"; + default: + return "minus"; + } + } +}); + +Report.reopenClass({ + fillMissingDates(report, options = {}) { + const dataField = options.dataField || "data"; + const filledField = options.filledField || "data"; + const startDate = options.startDate || "start_date"; + const endDate = options.endDate || "end_date"; + + if (_.isArray(report[dataField])) { + const startDateFormatted = moment + .utc(report[startDate]) + .locale("en") + .format("YYYY-MM-DD"); + const endDateFormatted = moment + .utc(report[endDate]) + .locale("en") + .format("YYYY-MM-DD"); + + if (report.modes[0] === "stacked_chart") { + report[filledField] = report[dataField].map(rep => { + return { + req: rep.req, + label: rep.label, + color: rep.color, + data: fillMissingDates( + JSON.parse(JSON.stringify(rep.data)), + startDateFormatted, + endDateFormatted + ) + }; + }); + } else { + report[filledField] = fillMissingDates( + JSON.parse(JSON.stringify(report[dataField])), + startDateFormatted, + endDateFormatted + ); + } + } + }, + + find(type, startDate, endDate, categoryId, groupId) { + return ajax("/admin/reports/" + type, { + data: { + start_date: startDate, + end_date: endDate, + category_id: categoryId, + group_id: groupId + } + }).then(json => { + // don’t fill for large multi column tables + // which are not date based + const modes = json.report.modes; + if (modes.length !== 1 && modes[0] !== "table") { + Report.fillMissingDates(json.report); + } + + const model = Report.create({ type: type }); + model.setProperties(json.report); + + if (json.report.related_report) { + // TODO: fillMissingDates if xaxis is date + const related = Report.create({ + type: json.report.related_report.type + }); + related.setProperties(json.report.related_report); + model.set("relatedReport", related); + } + + return model; + }); + } +}); + +export default Report; diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 deleted file mode 100644 index bd7074ced2..0000000000 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ /dev/null @@ -1,556 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { makeArray } from "discourse-common/lib/helpers"; -import { isEmpty } from "@ember/utils"; -import EmberObject from "@ember/object"; -import { escapeExpression } from "discourse/lib/utilities"; -import { ajax } from "discourse/lib/ajax"; -import round from "discourse/lib/round"; -import { - fillMissingDates, - formatUsername, - toNumber -} from "discourse/lib/utilities"; -import { number, durationTiny } from "discourse/lib/formatter"; -import { renderAvatar } from "discourse/helpers/user-avatar"; - -// Change this line each time report format change -// and you want to ensure cache is reset -export const SCHEMA_VERSION = 4; - -const Report = EmberObject.extend({ - average: false, - percent: false, - higher_is_better: true, - - @discourseComputed("type", "start_date", "end_date") - reportUrl(type, start_date, end_date) { - start_date = moment - .utc(start_date) - .locale("en") - .format("YYYY-MM-DD"); - - end_date = moment - .utc(end_date) - .locale("en") - .format("YYYY-MM-DD"); - - return Discourse.getURL( - `/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}` - ); - }, - - valueAt(numDaysAgo) { - if (this.data) { - const wantedDate = moment() - .subtract(numDaysAgo, "days") - .locale("en") - .format("YYYY-MM-DD"); - const item = this.data.find(d => d.x === wantedDate); - if (item) { - return item.y; - } - } - return 0; - }, - - valueFor(startDaysAgo, endDaysAgo) { - if (this.data) { - const earliestDate = moment() - .subtract(endDaysAgo, "days") - .startOf("day"); - const latestDate = moment() - .subtract(startDaysAgo, "days") - .startOf("day"); - let d, - sum = 0, - count = 0; - this.data.forEach(datum => { - d = moment(datum.x); - if (d >= earliestDate && d <= latestDate) { - sum += datum.y; - count++; - } - }); - if (this.method === "average" && count > 0) { - sum /= count; - } - return round(sum, -2); - } - }, - - @discourseComputed("data", "average") - todayCount() { - return this.valueAt(0); - }, - - @discourseComputed("data", "average") - yesterdayCount() { - return this.valueAt(1); - }, - - @discourseComputed("data", "average") - sevenDaysAgoCount() { - return this.valueAt(7); - }, - - @discourseComputed("data", "average") - thirtyDaysAgoCount() { - return this.valueAt(30); - }, - - @discourseComputed("data", "average") - lastSevenDaysCount() { - return this.averageCount(7, this.valueFor(1, 7)); - }, - - @discourseComputed("data", "average") - lastThirtyDaysCount() { - return this.averageCount(30, this.valueFor(1, 30)); - }, - - averageCount(count, value) { - return this.average ? value / count : value; - }, - - @discourseComputed("yesterdayCount", "higher_is_better") - yesterdayTrend(yesterdayCount, higherIsBetter) { - return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter); - }, - - @discourseComputed("lastSevenDaysCount", "higher_is_better") - sevenDaysTrend(lastSevenDaysCount, higherIsBetter) { - return this._computeTrend( - this.valueFor(8, 14), - lastSevenDaysCount, - higherIsBetter - ); - }, - - @discourseComputed("data") - currentTotal(data) { - return data.reduce((cur, pair) => cur + pair.y, 0); - }, - - @discourseComputed("data", "currentTotal") - currentAverage(data, total) { - return makeArray(data).length === 0 - ? 0 - : parseFloat((total / parseFloat(data.length)).toFixed(1)); - }, - - @discourseComputed("trend", "higher_is_better") - trendIcon(trend, higherIsBetter) { - return this._iconForTrend(trend, higherIsBetter); - }, - - @discourseComputed("sevenDaysTrend", "higher_is_better") - sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) { - return this._iconForTrend(sevenDaysTrend, higherIsBetter); - }, - - @discourseComputed("thirtyDaysTrend", "higher_is_better") - thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) { - return this._iconForTrend(thirtyDaysTrend, higherIsBetter); - }, - - @discourseComputed("yesterdayTrend", "higher_is_better") - yesterdayTrendIcon(yesterdayTrend, higherIsBetter) { - return this._iconForTrend(yesterdayTrend, higherIsBetter); - }, - - @discourseComputed( - "prev_period", - "currentTotal", - "currentAverage", - "higher_is_better" - ) - trend(prev, currentTotal, currentAverage, higherIsBetter) { - const total = this.average ? currentAverage : currentTotal; - return this._computeTrend(prev, total, higherIsBetter); - }, - - @discourseComputed("prev30Days", "lastThirtyDaysCount", "higher_is_better") - thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) { - return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter); - }, - - @discourseComputed("type") - method(type) { - if (type === "time_to_first_response") { - return "average"; - } else { - return "sum"; - } - }, - - percentChangeString(val1, val2) { - const change = this._computeChange(val1, val2); - - if (isNaN(change) || !isFinite(change)) { - return null; - } else if (change > 0) { - return "+" + change.toFixed(0) + "%"; - } else { - return change.toFixed(0) + "%"; - } - }, - - @discourseComputed("prev_period", "currentTotal", "currentAverage") - trendTitle(prev, currentTotal, currentAverage) { - let current = this.average ? currentAverage : currentTotal; - let percent = this.percentChangeString(prev, current); - - if (this.average) { - prev = prev ? prev.toFixed(1) : "0"; - if (this.percent) { - current += "%"; - prev += "%"; - } - } else { - prev = number(prev); - current = number(current); - } - - return I18n.t("admin.dashboard.reports.trend_title", { - percent, - prev, - current - }); - }, - - changeTitle(valAtT1, valAtT2, prevPeriodString) { - const change = this.percentChangeString(valAtT1, valAtT2); - let title = ""; - if (change) { - title += `${change} change. `; - } - title += `Was ${number(valAtT1)} ${prevPeriodString}.`; - return title; - }, - - @discourseComputed("yesterdayCount") - yesterdayCountTitle(yesterdayCount) { - return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago"); - }, - - @discourseComputed("lastSevenDaysCount") - sevenDaysCountTitle(lastSevenDaysCount) { - return this.changeTitle( - this.valueFor(8, 14), - lastSevenDaysCount, - "two weeks ago" - ); - }, - - @discourseComputed("prev30Days", "lastThirtyDaysCount") - thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) { - return this.changeTitle( - prev30Days, - lastThirtyDaysCount, - "in the previous 30 day period" - ); - }, - - @discourseComputed("data") - sortedData(data) { - return this.xAxisIsDate ? data.toArray().reverse() : data.toArray(); - }, - - @discourseComputed("data") - xAxisIsDate() { - if (!this.data[0]) return false; - return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); - }, - - @discourseComputed("labels") - computedLabels(labels) { - return labels.map(label => { - const type = label.type || "string"; - - let mainProperty; - if (label.property) mainProperty = label.property; - else if (type === "user") mainProperty = label.properties["username"]; - else if (type === "topic") mainProperty = label.properties["title"]; - else if (type === "post") - mainProperty = label.properties["truncated_raw"]; - else mainProperty = label.properties[0]; - - return { - title: label.title, - sortProperty: label.sort_property || mainProperty, - mainProperty, - type, - compute: (row, opts = {}) => { - let value = null; - - if (opts.useSortProperty) { - value = row[label.sort_property || mainProperty]; - } else { - value = row[mainProperty]; - } - - if (type === "user") return this._userLabel(label.properties, row); - if (type === "post") return this._postLabel(label.properties, row); - if (type === "topic") return this._topicLabel(label.properties, row); - if (type === "seconds") return this._secondsLabel(value); - if (type === "link") return this._linkLabel(label.properties, row); - if (type === "percent") return this._percentLabel(value); - if (type === "bytes") return this._bytesLabel(value); - if (type === "number") { - return this._numberLabel(value, opts); - } - if (type === "date") { - const date = moment(value); - if (date.isValid()) return this._dateLabel(value, date); - } - if (type === "precise_date") { - const date = moment(value); - if (date.isValid()) return this._dateLabel(value, date, "LLL"); - } - if (type === "text") return this._textLabel(value); - - return { - value, - type, - property: mainProperty, - formatedValue: value ? escapeExpression(value) : "—" - }; - } - }; - }); - }, - - _userLabel(properties, row) { - const username = row[properties.username]; - - const formatedValue = () => { - const userId = row[properties.id]; - - const user = EmberObject.create({ - username, - name: formatUsername(username), - avatar_template: row[properties.avatar] - }); - - const href = Discourse.getURL(`/admin/users/${userId}/${username}`); - - const avatarImg = renderAvatar(user, { - imageSize: "tiny", - ignoreTitle: true - }); - - return `${avatarImg}${user.name}`; - }; - - return { - value: username, - formatedValue: username ? formatedValue(username) : "—" - }; - }, - - _topicLabel(properties, row) { - const topicTitle = row[properties.title]; - - const formatedValue = () => { - const topicId = row[properties.id]; - const href = Discourse.getURL(`/t/-/${topicId}`); - return `${escapeExpression(topicTitle)}`; - }; - - return { - value: topicTitle, - formatedValue: topicTitle ? formatedValue() : "—" - }; - }, - - _postLabel(properties, row) { - const postTitle = row[properties.truncated_raw]; - const postNumber = row[properties.number]; - const topicId = row[properties.topic_id]; - const href = Discourse.getURL(`/t/-/${topicId}/${postNumber}`); - - return { - property: properties.title, - value: postTitle, - formatedValue: - postTitle && href - ? `${escapeExpression(postTitle)}` - : "—" - }; - }, - - _secondsLabel(value) { - return { - value: toNumber(value), - formatedValue: durationTiny(value) - }; - }, - - _percentLabel(value) { - return { - value: toNumber(value), - formatedValue: value ? `${value}%` : "—" - }; - }, - - _numberLabel(value, options = {}) { - const formatNumbers = isEmpty(options.formatNumbers) - ? true - : options.formatNumbers; - - const formatedValue = () => (formatNumbers ? number(value) : value); - - return { - value: toNumber(value), - formatedValue: value ? formatedValue() : "—" - }; - }, - - _bytesLabel(value) { - return { - value: toNumber(value), - formatedValue: I18n.toHumanSize(value) - }; - }, - - _dateLabel(value, date, format = "LL") { - return { - value, - formatedValue: value ? date.format(format) : "—" - }; - }, - - _textLabel(value) { - const escaped = escapeExpression(value); - - return { - value, - formatedValue: value ? escaped : "—" - }; - }, - - _linkLabel(properties, row) { - const property = properties[0]; - const value = Discourse.getURL(row[property]); - const formatedValue = (href, anchor) => { - return `${escapeExpression( - anchor - )}`; - }; - - return { - value, - formatedValue: value ? formatedValue(value, row[properties[1]]) : "—" - }; - }, - - _computeChange(valAtT1, valAtT2) { - return ((valAtT2 - valAtT1) / valAtT1) * 100; - }, - - _computeTrend(valAtT1, valAtT2, higherIsBetter) { - const change = this._computeChange(valAtT1, valAtT2); - - if (change > 50) { - return higherIsBetter ? "high-trending-up" : "high-trending-down"; - } else if (change > 2) { - return higherIsBetter ? "trending-up" : "trending-down"; - } else if (change <= 2 && change >= -2) { - return "no-change"; - } else if (change < -50) { - return higherIsBetter ? "high-trending-down" : "high-trending-up"; - } else if (change < -2) { - return higherIsBetter ? "trending-down" : "trending-up"; - } - }, - - _iconForTrend(trend, higherIsBetter) { - switch (trend) { - case "trending-up": - return higherIsBetter ? "angle-up" : "angle-down"; - case "trending-down": - return higherIsBetter ? "angle-down" : "angle-up"; - case "high-trending-up": - return higherIsBetter ? "angle-double-up" : "angle-double-down"; - case "high-trending-down": - return higherIsBetter ? "angle-double-down" : "angle-double-up"; - default: - return "minus"; - } - } -}); - -Report.reopenClass({ - fillMissingDates(report, options = {}) { - const dataField = options.dataField || "data"; - const filledField = options.filledField || "data"; - const startDate = options.startDate || "start_date"; - const endDate = options.endDate || "end_date"; - - if (_.isArray(report[dataField])) { - const startDateFormatted = moment - .utc(report[startDate]) - .locale("en") - .format("YYYY-MM-DD"); - const endDateFormatted = moment - .utc(report[endDate]) - .locale("en") - .format("YYYY-MM-DD"); - - if (report.modes[0] === "stacked_chart") { - report[filledField] = report[dataField].map(rep => { - return { - req: rep.req, - label: rep.label, - color: rep.color, - data: fillMissingDates( - JSON.parse(JSON.stringify(rep.data)), - startDateFormatted, - endDateFormatted - ) - }; - }); - } else { - report[filledField] = fillMissingDates( - JSON.parse(JSON.stringify(report[dataField])), - startDateFormatted, - endDateFormatted - ); - } - } - }, - - find(type, startDate, endDate, categoryId, groupId) { - return ajax("/admin/reports/" + type, { - data: { - start_date: startDate, - end_date: endDate, - category_id: categoryId, - group_id: groupId - } - }).then(json => { - // don’t fill for large multi column tables - // which are not date based - const modes = json.report.modes; - if (modes.length !== 1 && modes[0] !== "table") { - Report.fillMissingDates(json.report); - } - - const model = Report.create({ type: type }); - model.setProperties(json.report); - - if (json.report.related_report) { - // TODO: fillMissingDates if xaxis is date - const related = Report.create({ - type: json.report.related_report.type - }); - related.setProperties(json.report.related_report); - model.set("relatedReport", related); - } - - return model; - }); - } -}); - -export default Report; 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..df2fa40ff9 --- /dev/null +++ b/app/assets/javascripts/admin/models/screened-email.js @@ -0,0 +1,31 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/models/screened-ip-address.js new file mode 100644 index 0000000000..585acb9b79 --- /dev/null +++ b/app/assets/javascripts/admin/models/screened-ip-address.js @@ -0,0 +1,54 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { equal } from "@ember/object/computed"; +import { ajax } from "discourse/lib/ajax"; +import EmberObject from "@ember/object"; + +const ScreenedIpAddress = EmberObject.extend({ + @discourseComputed("action_name") + actionName(actionName) { + return I18n.t(`admin.logs.screened_ips.actions.${actionName}`); + }, + + isBlocked: equal("action_name", "block"), + + @discourseComputed("ip_address") + isRange(ipAddress) { + return ipAddress.indexOf("/") > 0; + }, + + save() { + return ajax( + "/admin/logs/screened_ip_addresses" + + (this.id ? "/" + this.id : "") + + ".json", + { + type: this.id ? "PUT" : "POST", + data: { + ip_address: this.ip_address, + action_name: this.action_name + } + } + ); + }, + + destroy() { + return ajax("/admin/logs/screened_ip_addresses/" + this.id + ".json", { + type: "DELETE" + }); + } +}); + +ScreenedIpAddress.reopenClass({ + findAll(filter) { + return ajax("/admin/logs/screened_ip_addresses.json", { + data: { filter: filter } + }).then(screened_ips => screened_ips.map(b => ScreenedIpAddress.create(b))); + }, + + rollUp() { + return ajax("/admin/logs/screened_ip_addresses/roll_up", { type: "POST" }); + } +}); + +export default ScreenedIpAddress; diff --git a/app/assets/javascripts/admin/models/screened-ip-address.js.es6 b/app/assets/javascripts/admin/models/screened-ip-address.js.es6 deleted file mode 100644 index bfac17d86c..0000000000 --- a/app/assets/javascripts/admin/models/screened-ip-address.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { equal } from "@ember/object/computed"; -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const ScreenedIpAddress = EmberObject.extend({ - @discourseComputed("action_name") - actionName(actionName) { - return I18n.t(`admin.logs.screened_ips.actions.${actionName}`); - }, - - isBlocked: equal("action_name", "block"), - - @discourseComputed("ip_address") - isRange(ipAddress) { - return ipAddress.indexOf("/") > 0; - }, - - save() { - return ajax( - "/admin/logs/screened_ip_addresses" + - (this.id ? "/" + this.id : "") + - ".json", - { - type: this.id ? "PUT" : "POST", - data: { - ip_address: this.ip_address, - action_name: this.action_name - } - } - ); - }, - - destroy() { - return ajax("/admin/logs/screened_ip_addresses/" + this.id + ".json", { - type: "DELETE" - }); - } -}); - -ScreenedIpAddress.reopenClass({ - findAll(filter) { - return ajax("/admin/logs/screened_ip_addresses.json", { - data: { filter: filter } - }).then(screened_ips => screened_ips.map(b => ScreenedIpAddress.create(b))); - }, - - rollUp() { - return ajax("/admin/logs/screened_ip_addresses/roll_up", { type: "POST" }); - } -}); - -export default ScreenedIpAddress; diff --git a/app/assets/javascripts/admin/models/screened-url.js b/app/assets/javascripts/admin/models/screened-url.js new file mode 100644 index 0000000000..4cde3d1261 --- /dev/null +++ b/app/assets/javascripts/admin/models/screened-url.js @@ -0,0 +1,23 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { ajax } from "discourse/lib/ajax"; +import EmberObject from "@ember/object"; + +const ScreenedUrl = EmberObject.extend({ + @discourseComputed("action") + actionName(action) { + return I18n.t("admin.logs.screened_actions." + action); + } +}); + +ScreenedUrl.reopenClass({ + findAll: function() { + return ajax("/admin/logs/screened_urls.json").then(function(screened_urls) { + return screened_urls.map(function(b) { + return ScreenedUrl.create(b); + }); + }); + } +}); + +export default ScreenedUrl; diff --git a/app/assets/javascripts/admin/models/screened-url.js.es6 b/app/assets/javascripts/admin/models/screened-url.js.es6 deleted file mode 100644 index 31ea850778..0000000000 --- a/app/assets/javascripts/admin/models/screened-url.js.es6 +++ /dev/null @@ -1,22 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const ScreenedUrl = EmberObject.extend({ - @discourseComputed("action") - actionName(action) { - return I18n.t("admin.logs.screened_actions." + action); - } -}); - -ScreenedUrl.reopenClass({ - findAll: function() { - return ajax("/admin/logs/screened_urls.json").then(function(screened_urls) { - return screened_urls.map(function(b) { - return ScreenedUrl.create(b); - }); - }); - } -}); - -export default ScreenedUrl; diff --git a/app/assets/javascripts/admin/models/site-setting.js b/app/assets/javascripts/admin/models/site-setting.js new file mode 100644 index 0000000000..01e34566f5 --- /dev/null +++ b/app/assets/javascripts/admin/models/site-setting.js @@ -0,0 +1,42 @@ +import I18n from "I18n"; +import { ajax } from "discourse/lib/ajax"; +import Setting from "admin/mixins/setting-object"; +import EmberObject from "@ember/object"; + +const SiteSetting = EmberObject.extend(Setting, {}); + +SiteSetting.reopenClass({ + findAll() { + return ajax("/admin/site_settings").then(function(settings) { + // Group the results by category + const categories = {}; + settings.site_settings.forEach(function(s) { + if (!categories[s.category]) { + categories[s.category] = []; + } + categories[s.category].pushObject(SiteSetting.create(s)); + }); + + return Object.keys(categories).map(function(n) { + return { + nameKey: n, + name: I18n.t("admin.site_settings.categories." + n), + siteSettings: categories[n] + }; + }); + }); + }, + + update(key, value, opts = {}) { + const data = {}; + data[key] = value; + + if (opts["updateExistingUsers"] === true) { + data["updateExistingUsers"] = true; + } + + return ajax(`/admin/site_settings/${key}`, { type: "PUT", data }); + } +}); + +export default SiteSetting; diff --git a/app/assets/javascripts/admin/models/site-setting.js.es6 b/app/assets/javascripts/admin/models/site-setting.js.es6 deleted file mode 100644 index 4edc89a1b9..0000000000 --- a/app/assets/javascripts/admin/models/site-setting.js.es6 +++ /dev/null @@ -1,41 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import Setting from "admin/mixins/setting-object"; -import EmberObject from "@ember/object"; - -const SiteSetting = EmberObject.extend(Setting, {}); - -SiteSetting.reopenClass({ - findAll() { - return ajax("/admin/site_settings").then(function(settings) { - // Group the results by category - const categories = {}; - settings.site_settings.forEach(function(s) { - if (!categories[s.category]) { - categories[s.category] = []; - } - categories[s.category].pushObject(SiteSetting.create(s)); - }); - - return Object.keys(categories).map(function(n) { - return { - nameKey: n, - name: I18n.t("admin.site_settings.categories." + n), - siteSettings: categories[n] - }; - }); - }); - }, - - update(key, value, opts = {}) { - const data = {}; - data[key] = value; - - if (opts["updateExistingUsers"] === true) { - data["updateExistingUsers"] = true; - } - - return ajax(`/admin/site_settings/${key}`, { type: "PUT", data }); - } -}); - -export default SiteSetting; 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 b/app/assets/javascripts/admin/models/staff-action-log.js new file mode 100644 index 0000000000..a077d054c0 --- /dev/null +++ b/app/assets/javascripts/admin/models/staff-action-log.js @@ -0,0 +1,107 @@ +import I18n from "I18n"; +import discourseComputed from "discourse-common/utils/decorators"; +import { ajax } from "discourse/lib/ajax"; +import AdminUser from "admin/models/admin-user"; +import { escapeExpression } from "discourse/lib/utilities"; +import RestModel from "discourse/models/rest"; + +function format(label, value, escape = true) { + return value + ? `${I18n.t(label)}: ${escape ? escapeExpression(value) : value}` + : ""; +} + +const StaffActionLog = RestModel.extend({ + showFullDetails: false, + + @discourseComputed("action_name") + actionName(actionName) { + return I18n.t(`admin.logs.staff_actions.actions.${actionName}`); + }, + + @discourseComputed( + "email", + "ip_address", + "topic_id", + "post_id", + "category_id", + "new_value", + "previous_value", + "details", + "useCustomModalForDetails", + "useModalForDetails" + ) + formattedDetails( + email, + ipAddress, + topicId, + postId, + categoryId, + newValue, + previousValue, + details, + useCustomModalForDetails, + useModalForDetails + ) { + const postLink = postId + ? `${postId}` + : null; + + let lines = [ + format("email", email), + format("admin.logs.ip_address", ipAddress), + format("admin.logs.topic_id", topicId), + format("admin.logs.post_id", postLink, false), + format("admin.logs.category_id", categoryId) + ]; + + if (!useCustomModalForDetails) { + lines.push(format("admin.logs.staff_actions.new_value", newValue)); + lines.push( + format("admin.logs.staff_actions.previous_value", previousValue) + ); + } + + if (!useModalForDetails && details) { + lines = [...lines, ...escapeExpression(details).split("\n")]; + } + + const formatted = lines.filter(l => l.length > 0).join("
"); + return formatted.length > 0 ? formatted + "
" : ""; + }, + + @discourseComputed("details") + useModalForDetails(details) { + return details && details.length > 100; + }, + + @discourseComputed("action_name") + useCustomModalForDetails(actionName) { + return ["change_theme", "delete_theme"].includes(actionName); + } +}); + +StaffActionLog.reopenClass({ + munge(json) { + if (json.acting_user) { + json.acting_user = AdminUser.create(json.acting_user); + } + if (json.target_user) { + json.target_user = AdminUser.create(json.target_user); + } + return json; + }, + + findAll(data) { + return ajax("/admin/logs/staff_action_logs.json", { data }).then(result => { + return { + staff_action_logs: result.staff_action_logs.map(s => + StaffActionLog.create(s) + ), + user_history_actions: result.user_history_actions + }; + }); + } +}); + +export default StaffActionLog; diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 deleted file mode 100644 index 45330b13fc..0000000000 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ /dev/null @@ -1,106 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { ajax } from "discourse/lib/ajax"; -import AdminUser from "admin/models/admin-user"; -import { escapeExpression } from "discourse/lib/utilities"; -import RestModel from "discourse/models/rest"; - -function format(label, value, escape = true) { - return value - ? `${I18n.t(label)}: ${escape ? escapeExpression(value) : value}` - : ""; -} - -const StaffActionLog = RestModel.extend({ - showFullDetails: false, - - @discourseComputed("action_name") - actionName(actionName) { - return I18n.t(`admin.logs.staff_actions.actions.${actionName}`); - }, - - @discourseComputed( - "email", - "ip_address", - "topic_id", - "post_id", - "category_id", - "new_value", - "previous_value", - "details", - "useCustomModalForDetails", - "useModalForDetails" - ) - formattedDetails( - email, - ipAddress, - topicId, - postId, - categoryId, - newValue, - previousValue, - details, - useCustomModalForDetails, - useModalForDetails - ) { - const postLink = postId - ? `${postId}` - : null; - - let lines = [ - format("email", email), - format("admin.logs.ip_address", ipAddress), - format("admin.logs.topic_id", topicId), - format("admin.logs.post_id", postLink, false), - format("admin.logs.category_id", categoryId) - ]; - - if (!useCustomModalForDetails) { - lines.push(format("admin.logs.staff_actions.new_value", newValue)); - lines.push( - format("admin.logs.staff_actions.previous_value", previousValue) - ); - } - - if (!useModalForDetails && details) { - lines = [...lines, ...escapeExpression(details).split("\n")]; - } - - const formatted = lines.filter(l => l.length > 0).join("
"); - return formatted.length > 0 ? formatted + "
" : ""; - }, - - @discourseComputed("details") - useModalForDetails(details) { - return details && details.length > 100; - }, - - @discourseComputed("action_name") - useCustomModalForDetails(actionName) { - return ["change_theme", "delete_theme"].includes(actionName); - } -}); - -StaffActionLog.reopenClass({ - munge(json) { - if (json.acting_user) { - json.acting_user = AdminUser.create(json.acting_user); - } - if (json.target_user) { - json.target_user = AdminUser.create(json.target_user); - } - return json; - }, - - findAll(data) { - return ajax("/admin/logs/staff_action_logs.json", { data }).then(result => { - return { - staff_action_logs: result.staff_action_logs.map(s => - StaffActionLog.create(s) - ), - user_history_actions: result.user_history_actions - }; - }); - } -}); - -export default StaffActionLog; 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..0b96660f99 --- /dev/null +++ b/app/assets/javascripts/admin/models/theme.js @@ -0,0 +1,353 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/models/tl3-requirements.js new file mode 100644 index 0000000000..6331333de7 --- /dev/null +++ b/app/assets/javascripts/admin/models/tl3-requirements.js @@ -0,0 +1,75 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import EmberObject from "@ember/object"; + +export default EmberObject.extend({ + @discourseComputed("days_visited", "time_period") + days_visited_percent(daysVisited, timePeriod) { + return Math.round((daysVisited * 100) / timePeriod); + }, + + @discourseComputed("min_days_visited", "time_period") + min_days_visited_percent(minDaysVisited, timePeriod) { + return Math.round((minDaysVisited * 100) / timePeriod); + }, + + @discourseComputed("num_topics_replied_to", "min_topics_replied_to") + capped_topics_replied_to(numReplied, minReplied) { + return numReplied > minReplied; + }, + + @discourseComputed( + "days_visited", + "min_days_visited", + "num_topics_replied_to", + "min_topics_replied_to", + "topics_viewed", + "min_topics_viewed", + "posts_read", + "min_posts_read", + "num_flagged_posts", + "max_flagged_posts", + "topics_viewed_all_time", + "min_topics_viewed_all_time", + "posts_read_all_time", + "min_posts_read_all_time", + "num_flagged_by_users", + "max_flagged_by_users", + "num_likes_given", + "min_likes_given", + "num_likes_received", + "min_likes_received", + "num_likes_received", + "min_likes_received", + "num_likes_received_days", + "min_likes_received_days", + "num_likes_received_users", + "min_likes_received_users", + "trust_level_locked", + "penalty_counts.silenced", + "penalty_counts.suspended" + ) + met() { + return { + days_visited: this.days_visited >= this.min_days_visited, + topics_replied_to: + this.num_topics_replied_to >= this.min_topics_replied_to, + topics_viewed: this.topics_viewed >= this.min_topics_viewed, + posts_read: this.posts_read >= this.min_posts_read, + topics_viewed_all_time: + this.topics_viewed_all_time >= this.min_topics_viewed_all_time, + posts_read_all_time: + this.posts_read_all_time >= this.min_posts_read_all_time, + flagged_posts: this.num_flagged_posts <= this.max_flagged_posts, + flagged_by_users: this.num_flagged_by_users <= this.max_flagged_by_users, + likes_given: this.num_likes_given >= this.min_likes_given, + likes_received: this.num_likes_received >= this.min_likes_received, + likes_received_days: + this.num_likes_received_days >= this.min_likes_received_days, + likes_received_users: + this.num_likes_received_users >= this.min_likes_received_users, + level_locked: this.trust_level_locked, + silenced: this.get("penalty_counts.silenced") === 0, + suspended: this.get("penalty_counts.suspended") === 0 + }; + } +}); diff --git a/app/assets/javascripts/admin/models/tl3-requirements.js.es6 b/app/assets/javascripts/admin/models/tl3-requirements.js.es6 deleted file mode 100644 index 424aea4f58..0000000000 --- a/app/assets/javascripts/admin/models/tl3-requirements.js.es6 +++ /dev/null @@ -1,70 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import EmberObject from "@ember/object"; - -export default EmberObject.extend({ - @discourseComputed("days_visited", "time_period") - days_visited_percent(daysVisited, timePeriod) { - return Math.round((daysVisited * 100) / timePeriod); - }, - - @discourseComputed("min_days_visited", "time_period") - min_days_visited_percent(minDaysVisited, timePeriod) { - return Math.round((minDaysVisited * 100) / timePeriod); - }, - - @discourseComputed( - "days_visited", - "min_days_visited", - "num_topics_replied_to", - "min_topics_replied_to", - "topics_viewed", - "min_topics_viewed", - "posts_read", - "min_posts_read", - "num_flagged_posts", - "max_flagged_posts", - "topics_viewed_all_time", - "min_topics_viewed_all_time", - "posts_read_all_time", - "min_posts_read_all_time", - "num_flagged_by_users", - "max_flagged_by_users", - "num_likes_given", - "min_likes_given", - "num_likes_received", - "min_likes_received", - "num_likes_received", - "min_likes_received", - "num_likes_received_days", - "min_likes_received_days", - "num_likes_received_users", - "min_likes_received_users", - "trust_level_locked", - "penalty_counts.silenced", - "penalty_counts.suspended" - ) - met() { - return { - days_visited: this.days_visited >= this.min_days_visited, - topics_replied_to: - this.num_topics_replied_to >= this.min_topics_replied_to, - topics_viewed: this.topics_viewed >= this.min_topics_viewed, - posts_read: this.posts_read >= this.min_posts_read, - topics_viewed_all_time: - this.topics_viewed_all_time >= this.min_topics_viewed_all_time, - posts_read_all_time: - this.posts_read_all_time >= this.min_posts_read_all_time, - flagged_posts: this.num_flagged_posts <= this.max_flagged_posts, - flagged_by_users: this.num_flagged_by_users <= this.max_flagged_by_users, - likes_given: this.num_likes_given >= this.min_likes_given, - likes_received: this.num_likes_received >= this.min_likes_received, - likes_received_days: - this.num_likes_received_days >= this.min_likes_received_days, - likes_received_users: - this.num_likes_received_users >= this.min_likes_received_users, - level_locked: this.trust_level_locked, - silenced: this.get("penalty_counts.silenced") === 0, - suspended: this.get("penalty_counts.suspended") === 0 - }; - } -}); 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 b/app/assets/javascripts/admin/models/watched-word.js new file mode 100644 index 0000000000..6713a8a605 --- /dev/null +++ b/app/assets/javascripts/admin/models/watched-word.js @@ -0,0 +1,55 @@ +import I18n from "I18n"; +import { ajax } from "discourse/lib/ajax"; +import EmberObject from "@ember/object"; + +const WatchedWord = EmberObject.extend({ + save() { + return ajax( + "/admin/logs/watched_words" + (this.id ? "/" + this.id : "") + ".json", + { + type: this.id ? "PUT" : "POST", + data: { word: this.word, action_key: this.action }, + dataType: "json" + } + ); + }, + + destroy() { + return ajax("/admin/logs/watched_words/" + this.id + ".json", { + type: "DELETE" + }); + } +}); + +WatchedWord.reopenClass({ + findAll() { + return ajax("/admin/logs/watched_words.json").then(list => { + const actions = {}; + list.words.forEach(s => { + if (!actions[s.action]) { + actions[s.action] = []; + } + actions[s.action].pushObject(WatchedWord.create(s)); + }); + + list.actions.forEach(a => { + if (!actions[a]) { + actions[a] = []; + } + }); + + return Object.keys(actions).map(n => { + return EmberObject.create({ + nameKey: n, + name: I18n.t("admin.watched_words.actions." + n), + words: actions[n], + count: actions[n].length, + regularExpressions: list.regular_expressions, + compiledRegularExpression: list.compiled_regular_expressions[n] + }); + }); + }); + } +}); + +export default WatchedWord; diff --git a/app/assets/javascripts/admin/models/watched-word.js.es6 b/app/assets/javascripts/admin/models/watched-word.js.es6 deleted file mode 100644 index dac78affe1..0000000000 --- a/app/assets/javascripts/admin/models/watched-word.js.es6 +++ /dev/null @@ -1,54 +0,0 @@ -import { ajax } from "discourse/lib/ajax"; -import EmberObject from "@ember/object"; - -const WatchedWord = EmberObject.extend({ - save() { - return ajax( - "/admin/logs/watched_words" + (this.id ? "/" + this.id : "") + ".json", - { - type: this.id ? "PUT" : "POST", - data: { word: this.word, action_key: this.action }, - dataType: "json" - } - ); - }, - - destroy() { - return ajax("/admin/logs/watched_words/" + this.id + ".json", { - type: "DELETE" - }); - } -}); - -WatchedWord.reopenClass({ - findAll() { - return ajax("/admin/logs/watched_words.json").then(list => { - const actions = {}; - list.words.forEach(s => { - if (!actions[s.action]) { - actions[s.action] = []; - } - actions[s.action].pushObject(WatchedWord.create(s)); - }); - - list.actions.forEach(a => { - if (!actions[a]) { - actions[a] = []; - } - }); - - return Object.keys(actions).map(n => { - return EmberObject.create({ - nameKey: n, - name: I18n.t("admin.watched_words.actions." + n), - words: actions[n], - count: actions[n].length, - regularExpressions: list.regular_expressions, - compiledRegularExpression: list.compiled_regular_expressions[n] - }); - }); - }); - } -}); - -export default WatchedWord; 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 b/app/assets/javascripts/admin/routes/admin-backups-logs.js new file mode 100644 index 0000000000..2c7e16eed7 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-backups-logs.js @@ -0,0 +1,31 @@ +import EmberObject from "@ember/object"; +import Route from "@ember/routing/route"; +import PreloadStore from "discourse/lib/preload-store"; + +export default Route.extend({ + // since the logs are pushed via the message bus + // we only want to preload them (hence the beforeModel hook) + beforeModel() { + const logs = this.controllerFor("adminBackupsLogs").get("logs"); + // preload the logs if any + PreloadStore.getAndRemove("logs").then(function(preloadedLogs) { + if (preloadedLogs && preloadedLogs.length) { + // we need to filter out message like: "[SUCCESS]" + // and convert POJOs to Ember Objects + const newLogs = _.chain(preloadedLogs) + .reject(function(log) { + return log.message.length === 0 || log.message[0] === "["; + }) + .map(function(log) { + return EmberObject.create(log); + }) + .value(); + logs.pushObjects(newLogs); + } + }); + }, + + setupController() { + /* prevent default behavior */ + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 deleted file mode 100644 index 3d24756a4a..0000000000 --- a/app/assets/javascripts/admin/routes/admin-backups-logs.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -import EmberObject from "@ember/object"; -import Route from "@ember/routing/route"; -import PreloadStore from "preload-store"; - -export default Route.extend({ - // since the logs are pushed via the message bus - // we only want to preload them (hence the beforeModel hook) - beforeModel() { - const logs = this.controllerFor("adminBackupsLogs").get("logs"); - // preload the logs if any - PreloadStore.getAndRemove("logs").then(function(preloadedLogs) { - if (preloadedLogs && preloadedLogs.length) { - // we need to filter out message like: "[SUCCESS]" - // and convert POJOs to Ember Objects - const newLogs = _.chain(preloadedLogs) - .reject(function(log) { - return log.message.length === 0 || log.message[0] === "["; - }) - .map(function(log) { - return EmberObject.create(log); - }) - .value(); - logs.pushObjects(newLogs); - } - }); - }, - - setupController() { - /* prevent default behavior */ - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-backups.js b/app/assets/javascripts/admin/routes/admin-backups.js new file mode 100644 index 0000000000..aa7c88aba9 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-backups.js @@ -0,0 +1,162 @@ +import getURL from "discourse-common/lib/get-url"; +import I18n from "I18n"; +import EmberObject from "@ember/object"; +import DiscourseRoute from "discourse/routes/discourse"; +import { ajax } from "discourse/lib/ajax"; +import showModal from "discourse/lib/show-modal"; +import BackupStatus from "admin/models/backup-status"; +import Backup from "admin/models/backup"; +import PreloadStore from "discourse/lib/preload-store"; +import User from "discourse/models/user"; + +const LOG_CHANNEL = "/admin/backups/logs"; + +export default DiscourseRoute.extend({ + activate() { + this.messageBus.subscribe(LOG_CHANNEL, log => { + if (log.message === "[STARTED]") { + User.currentProp("hideReadOnlyAlert", true); + this.controllerFor("adminBackups").set( + "model.isOperationRunning", + true + ); + this.controllerFor("adminBackupsLogs") + .get("logs") + .clear(); + } else if (log.message === "[FAILED]") { + this.controllerFor("adminBackups").set( + "model.isOperationRunning", + false + ); + bootbox.alert( + I18n.t("admin.backups.operations.failed", { + operation: log.operation + }) + ); + } else if (log.message === "[SUCCESS]") { + User.currentProp("hideReadOnlyAlert", false); + this.controllerFor("adminBackups").set( + "model.isOperationRunning", + false + ); + if (log.operation === "restore") { + // redirect to homepage when the restore is done (session might be lost) + window.location = getURL("/"); + } + } else { + this.controllerFor("adminBackupsLogs") + .get("logs") + .pushObject(EmberObject.create(log)); + } + }); + }, + + model() { + return PreloadStore.getAndRemove("operations_status", () => + ajax("/admin/backups/status.json") + ).then(status => + BackupStatus.create({ + isOperationRunning: status.is_operation_running, + canRollback: status.can_rollback, + allowRestore: status.allow_restore + }) + ); + }, + + deactivate() { + this.messageBus.unsubscribe(LOG_CHANNEL); + }, + + actions: { + showStartBackupModal() { + showModal("admin-start-backup", { admin: true }); + this.controllerFor("modal").set("modalClass", "start-backup-modal"); + }, + + startBackup(withUploads) { + this.transitionTo("admin.backups.logs"); + Backup.start(withUploads); + }, + + destroyBackup(backup) { + bootbox.confirm( + I18n.t("admin.backups.operations.destroy.confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + backup.destroy().then(() => + this.controllerFor("adminBackupsIndex") + .get("model") + .removeObject(backup) + ); + } + } + ); + }, + + startRestore(backup) { + bootbox.confirm( + I18n.t("admin.backups.operations.restore.confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + this.transitionTo("admin.backups.logs"); + backup.restore(); + } + } + ); + }, + + cancelOperation() { + bootbox.confirm( + I18n.t("admin.backups.operations.cancel.confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + Backup.cancel().then(() => { + this.controllerFor("adminBackups").set( + "model.isOperationRunning", + false + ); + }); + } + } + ); + }, + + rollback() { + bootbox.confirm( + I18n.t("admin.backups.operations.rollback.confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + Backup.rollback(); + } + } + ); + }, + + uploadSuccess(filename) { + bootbox.alert(I18n.t("admin.backups.upload.success", { filename })); + }, + + uploadError(filename, message) { + bootbox.alert( + I18n.t("admin.backups.upload.error", { filename, message }) + ); + }, + + remoteUploadSuccess() { + Backup.find().then(backups => { + this.controllerFor("adminBackupsIndex").set( + "model", + backups.map(backup => Backup.create(backup)) + ); + }); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 deleted file mode 100644 index 6bc42d725f..0000000000 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ /dev/null @@ -1,160 +0,0 @@ -import EmberObject from "@ember/object"; -import DiscourseRoute from "discourse/routes/discourse"; -import { ajax } from "discourse/lib/ajax"; -import showModal from "discourse/lib/show-modal"; -import BackupStatus from "admin/models/backup-status"; -import Backup from "admin/models/backup"; -import PreloadStore from "preload-store"; -import User from "discourse/models/user"; - -const LOG_CHANNEL = "/admin/backups/logs"; - -export default DiscourseRoute.extend({ - activate() { - this.messageBus.subscribe(LOG_CHANNEL, log => { - if (log.message === "[STARTED]") { - User.currentProp("hideReadOnlyAlert", true); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - true - ); - this.controllerFor("adminBackupsLogs") - .get("logs") - .clear(); - } else if (log.message === "[FAILED]") { - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - bootbox.alert( - I18n.t("admin.backups.operations.failed", { - operation: log.operation - }) - ); - } else if (log.message === "[SUCCESS]") { - User.currentProp("hideReadOnlyAlert", false); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - if (log.operation === "restore") { - // redirect to homepage when the restore is done (session might be lost) - window.location = Discourse.getURL("/"); - } - } else { - this.controllerFor("adminBackupsLogs") - .get("logs") - .pushObject(EmberObject.create(log)); - } - }); - }, - - model() { - return PreloadStore.getAndRemove("operations_status", () => - ajax("/admin/backups/status.json") - ).then(status => - BackupStatus.create({ - isOperationRunning: status.is_operation_running, - canRollback: status.can_rollback, - allowRestore: status.allow_restore - }) - ); - }, - - deactivate() { - this.messageBus.unsubscribe(LOG_CHANNEL); - }, - - actions: { - showStartBackupModal() { - showModal("admin-start-backup", { admin: true }); - this.controllerFor("modal").set("modalClass", "start-backup-modal"); - }, - - startBackup(withUploads) { - this.transitionTo("admin.backups.logs"); - Backup.start(withUploads); - }, - - destroyBackup(backup) { - bootbox.confirm( - I18n.t("admin.backups.operations.destroy.confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - backup.destroy().then(() => - this.controllerFor("adminBackupsIndex") - .get("model") - .removeObject(backup) - ); - } - } - ); - }, - - startRestore(backup) { - bootbox.confirm( - I18n.t("admin.backups.operations.restore.confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - this.transitionTo("admin.backups.logs"); - backup.restore(); - } - } - ); - }, - - cancelOperation() { - bootbox.confirm( - I18n.t("admin.backups.operations.cancel.confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - Backup.cancel().then(() => { - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - }); - } - } - ); - }, - - rollback() { - bootbox.confirm( - I18n.t("admin.backups.operations.rollback.confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - confirmed => { - if (confirmed) { - Backup.rollback(); - } - } - ); - }, - - uploadSuccess(filename) { - bootbox.alert(I18n.t("admin.backups.upload.success", { filename })); - }, - - uploadError(filename, message) { - bootbox.alert( - I18n.t("admin.backups.upload.error", { filename, message }) - ); - }, - - remoteUploadSuccess() { - Backup.find().then(backups => { - this.controllerFor("adminBackupsIndex").set( - "model", - backups.map(backup => Backup.create(backup)) - ); - }); - } - } -}); 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..7a74db7161 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-badges-show.js @@ -0,0 +1,64 @@ +import I18n from "I18n"; +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 b/app/assets/javascripts/admin/routes/admin-badges.js new file mode 100644 index 0000000000..ab6de98504 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-badges.js @@ -0,0 +1,42 @@ +import I18n from "I18n"; +import DiscourseRoute from "discourse/routes/discourse"; +import { ajax } from "discourse/lib/ajax"; +import Badge from "discourse/models/badge"; +import BadgeGrouping from "discourse/models/badge-grouping"; + +export default DiscourseRoute.extend({ + _json: null, + + model() { + return ajax("/admin/badges.json").then(json => { + this._json = json; + return Badge.createFromJson(json); + }); + }, + + setupController(controller, model) { + const json = this._json; + const badgeTriggers = []; + const badgeGroupings = []; + + Object.keys(json.admin_badges.triggers).forEach(k => { + const id = json.admin_badges.triggers[k]; + badgeTriggers.push({ + id, + name: I18n.t("admin.badges.trigger_type." + k) + }); + }); + + json.badge_groupings.forEach(function(badgeGroupingJson) { + badgeGroupings.push(BadgeGrouping.create(badgeGroupingJson)); + }); + + controller.setProperties({ + badgeGroupings: badgeGroupings, + badgeTypes: json.badge_types, + protectedSystemFields: json.admin_badges.protected_system_fields, + badgeTriggers, + model + }); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-badges.js.es6 b/app/assets/javascripts/admin/routes/admin-badges.js.es6 deleted file mode 100644 index 0dac4bbaa2..0000000000 --- a/app/assets/javascripts/admin/routes/admin-badges.js.es6 +++ /dev/null @@ -1,41 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; -import { ajax } from "discourse/lib/ajax"; -import Badge from "discourse/models/badge"; -import BadgeGrouping from "discourse/models/badge-grouping"; - -export default DiscourseRoute.extend({ - _json: null, - - model() { - return ajax("/admin/badges.json").then(json => { - this._json = json; - return Badge.createFromJson(json); - }); - }, - - setupController(controller, model) { - const json = this._json; - const badgeTriggers = []; - const badgeGroupings = []; - - Object.keys(json.admin_badges.triggers).forEach(k => { - const id = json.admin_badges.triggers[k]; - badgeTriggers.push({ - id, - name: I18n.t("admin.badges.trigger_type." + k) - }); - }); - - json.badge_groupings.forEach(function(badgeGroupingJson) { - badgeGroupings.push(BadgeGrouping.create(badgeGroupingJson)); - }); - - controller.setProperties({ - badgeGroupings: badgeGroupings, - badgeTypes: json.badge_types, - protectedSystemFields: json.admin_badges.protected_system_fields, - badgeTriggers, - model - }); - } -}); 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 b/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js new file mode 100644 index 0000000000..d3a2b035af --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js @@ -0,0 +1,41 @@ +import I18n from "I18n"; +import Route from "@ember/routing/route"; +export default Route.extend({ + model(params) { + return { + model: this.modelFor("adminCustomizeEmailStyle"), + fieldName: params.field_name + }; + }, + + setupController(controller, model) { + controller.setProperties({ + fieldName: model.fieldName, + model: model.model + }); + this._shouldAlertUnsavedChanges = true; + }, + + actions: { + willTransition(transition) { + if ( + this.get("controller.model.changed") && + this._shouldAlertUnsavedChanges && + transition.intent.name !== this.routeName + ) { + transition.abort(); + bootbox.confirm( + I18n.t("admin.customize.theme.unsaved_changes_alert"), + I18n.t("admin.customize.theme.discard"), + I18n.t("admin.customize.theme.stay"), + result => { + if (!result) { + this._shouldAlertUnsavedChanges = false; + transition.retry(); + } + } + ); + } + } + } +}); 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.es6 deleted file mode 100644 index 8dcc18f0d9..0000000000 --- a/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -import Route from "@ember/routing/route"; -export default Route.extend({ - model(params) { - return { - model: this.modelFor("adminCustomizeEmailStyle"), - fieldName: params.field_name - }; - }, - - setupController(controller, model) { - controller.setProperties({ - fieldName: model.fieldName, - model: model.model - }); - this._shouldAlertUnsavedChanges = true; - }, - - actions: { - willTransition(transition) { - if ( - this.get("controller.model.changed") && - this._shouldAlertUnsavedChanges && - transition.intent.name !== this.routeName - ) { - transition.abort(); - bootbox.confirm( - I18n.t("admin.customize.theme.unsaved_changes_alert"), - I18n.t("admin.customize.theme.discard"), - I18n.t("admin.customize.theme.stay"), - result => { - if (!result) { - this._shouldAlertUnsavedChanges = false; - transition.retry(); - } - } - ); - } - } - } -}); 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 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js new file mode 100644 index 0000000000..5cb695ddcd --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js @@ -0,0 +1,65 @@ +import I18n from "I18n"; +import Route from "@ember/routing/route"; +export default Route.extend({ + model(params) { + const all = this.modelFor("adminCustomizeThemes"); + const model = all.findBy("id", parseInt(params.theme_id, 10)); + return model + ? { + model, + target: params.target, + field_name: params.field_name + } + : this.replaceWith("adminCustomizeThemes.index"); + }, + + serialize(wrapper) { + return { + model: wrapper.model, + target: wrapper.target || "common", + field_name: wrapper.field_name || "scss", + theme_id: wrapper.model.get("id") + }; + }, + + setupController(controller, wrapper) { + const fields = wrapper.model.get("fields")[wrapper.target].map(f => f.name); + if (!fields.includes(wrapper.field_name)) { + this.transitionTo( + "adminCustomizeThemes.edit", + wrapper.model.id, + wrapper.target, + fields[0] + ); + return; + } + controller.set("model", wrapper.model); + controller.setTargetName(wrapper.target || "common"); + controller.set("fieldName", wrapper.field_name || "scss"); + this.controllerFor("adminCustomizeThemes").set("editingTheme", true); + this.set("shouldAlertUnsavedChanges", true); + }, + + actions: { + willTransition(transition) { + if ( + this.get("controller.model.changed") && + this.shouldAlertUnsavedChanges && + transition.intent.name !== this.routeName + ) { + transition.abort(); + bootbox.confirm( + I18n.t("admin.customize.theme.unsaved_changes_alert"), + I18n.t("admin.customize.theme.discard"), + I18n.t("admin.customize.theme.stay"), + result => { + if (!result) { + this.set("shouldAlertUnsavedChanges", false); + transition.retry(); + } + } + ); + } + } + } +}); 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.es6 deleted file mode 100644 index 62a70f0d7c..0000000000 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -import Route from "@ember/routing/route"; -export default Route.extend({ - model(params) { - const all = this.modelFor("adminCustomizeThemes"); - const model = all.findBy("id", parseInt(params.theme_id, 10)); - return model - ? { - model, - target: params.target, - field_name: params.field_name - } - : this.replaceWith("adminCustomizeThemes.index"); - }, - - serialize(wrapper) { - return { - model: wrapper.model, - target: wrapper.target || "common", - field_name: wrapper.field_name || "scss", - theme_id: wrapper.model.get("id") - }; - }, - - setupController(controller, wrapper) { - const fields = wrapper.model.get("fields")[wrapper.target].map(f => f.name); - if (!fields.includes(wrapper.field_name)) { - this.transitionTo( - "adminCustomizeThemes.edit", - wrapper.model.id, - wrapper.target, - fields[0] - ); - return; - } - controller.set("model", wrapper.model); - controller.setTargetName(wrapper.target || "common"); - controller.set("fieldName", wrapper.field_name || "scss"); - this.controllerFor("adminCustomizeThemes").set("editingTheme", true); - this.set("shouldAlertUnsavedChanges", true); - }, - - actions: { - willTransition(transition) { - if ( - this.get("controller.model.changed") && - this.shouldAlertUnsavedChanges && - transition.intent.name !== this.routeName - ) { - transition.abort(); - bootbox.confirm( - I18n.t("admin.customize.theme.unsaved_changes_alert"), - I18n.t("admin.customize.theme.discard"), - I18n.t("admin.customize.theme.stay"), - result => { - if (!result) { - this.set("shouldAlertUnsavedChanges", false); - transition.retry(); - } - } - ); - } - } - } -}); 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 b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js new file mode 100644 index 0000000000..ee175a9df8 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js @@ -0,0 +1,74 @@ +import I18n from "I18n"; +import Route from "@ember/routing/route"; +import { scrollTop } from "discourse/mixins/scroll-top"; +import { THEMES, COMPONENTS } from "admin/models/theme"; + +export default Route.extend({ + serialize(model) { + return { theme_id: model.get("id") }; + }, + + model(params) { + const all = this.modelFor("adminCustomizeThemes"); + const model = all.findBy("id", parseInt(params.theme_id, 10)); + return model ? model : this.replaceWith("adminCustomizeTheme.index"); + }, + + setupController(controller, model) { + this._super(...arguments); + + const parentController = this.controllerFor("adminCustomizeThemes"); + + parentController.setProperties({ + editingTheme: false, + currentTab: model.get("component") ? COMPONENTS : THEMES + }); + + controller.setProperties({ + model: model, + parentController: parentController, + allThemes: parentController.get("model"), + colorSchemeId: model.get("color_scheme_id"), + colorSchemes: parentController.get("model.extras.color_schemes"), + editingName: false + }); + + this.handleHighlight(model); + }, + + deactivate() { + this.handleHighlight(); + }, + + handleHighlight(theme) { + this.get("controller.allThemes") + .filter(t => t.get("selected")) + .forEach(t => t.set("selected", false)); + if (theme) { + theme.set("selected", true); + } + }, + + actions: { + didTransition() { + scrollTop(); + }, + willTransition(transition) { + const model = this.controller.model; + if (model.recentlyInstalled && !model.hasParents && model.component) { + transition.abort(); + bootbox.confirm( + I18n.t("admin.customize.theme.unsaved_parent_themes"), + I18n.t("admin.customize.theme.discard"), + I18n.t("admin.customize.theme.stay"), + result => { + if (!result) { + this.controller.model.setProperties({ recentlyInstalled: false }); + transition.retry(); + } + } + ); + } + } + } +}); 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.es6 deleted file mode 100644 index 5b9ce25169..0000000000 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 +++ /dev/null @@ -1,73 +0,0 @@ -import Route from "@ember/routing/route"; -import { scrollTop } from "discourse/mixins/scroll-top"; -import { THEMES, COMPONENTS } from "admin/models/theme"; - -export default Route.extend({ - serialize(model) { - return { theme_id: model.get("id") }; - }, - - model(params) { - const all = this.modelFor("adminCustomizeThemes"); - const model = all.findBy("id", parseInt(params.theme_id, 10)); - return model ? model : this.replaceWith("adminCustomizeTheme.index"); - }, - - setupController(controller, model) { - this._super(...arguments); - - const parentController = this.controllerFor("adminCustomizeThemes"); - - parentController.setProperties({ - editingTheme: false, - currentTab: model.get("component") ? COMPONENTS : THEMES - }); - - controller.setProperties({ - model: model, - parentController: parentController, - allThemes: parentController.get("model"), - colorSchemeId: model.get("color_scheme_id"), - colorSchemes: parentController.get("model.extras.color_schemes"), - editingName: false - }); - - this.handleHighlight(model); - }, - - deactivate() { - this.handleHighlight(); - }, - - handleHighlight(theme) { - this.get("controller.allThemes") - .filter(t => t.get("selected")) - .forEach(t => t.set("selected", false)); - if (theme) { - theme.set("selected", true); - } - }, - - actions: { - didTransition() { - scrollTop(); - }, - willTransition(transition) { - const model = this.controller.model; - if (model.recentlyInstalled && !model.hasParents && model.component) { - transition.abort(); - bootbox.confirm( - I18n.t("admin.customize.theme.unsaved_parent_themes"), - I18n.t("admin.customize.theme.discard"), - I18n.t("admin.customize.theme.stay"), - result => { - if (!result) { - this.controller.model.setProperties({ recentlyInstalled: false }); - transition.retry(); - } - } - ); - } - } - } -}); 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 b/app/assets/javascripts/admin/routes/admin-plugins.js new file mode 100644 index 0000000000..261610a97e --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-plugins.js @@ -0,0 +1,22 @@ +import Route from "@ember/routing/route"; +export default Route.extend({ + model() { + return this.store.findAll("plugin"); + }, + + actions: { + showSettings(plugin) { + const controller = this.controllerFor("adminSiteSettings"); + this.transitionTo("adminSiteSettingsCategory", "plugins").then(() => { + if (plugin) { + // filterContent() is normally on a debounce from typing. + // Because we don't want the default of "All Results", we tell it + // to skip the next debounce. + controller.set("filter", `plugin:${plugin.id}`); + controller.set("_skipBounce", true); + controller.filterContentNow("plugins"); + } + }); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 deleted file mode 100644 index c4af286faf..0000000000 --- a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -import Route from "@ember/routing/route"; -export default Route.extend({ - model() { - return this.store.findAll("plugin"); - }, - - actions: { - showSettings(plugin) { - const controller = this.controllerFor("adminSiteSettings"); - this.transitionTo("adminSiteSettingsCategory", "plugins").then(() => { - if (plugin) { - const siteSettingFilter = plugin.get("enabled_setting_filter"); - const match = /^(.*)_enabled/.exec(plugin.get("enabled_setting")); - const filter = siteSettingFilter || match[1]; - - if (filter) { - // filterContent() is normally on a debounce from typing. - // Because we don't want the default of "All Results", we tell it - // to skip the next debounce. - controller.set("filter", filter); - controller.set("_skipBounce", true); - controller.filterContentNow("plugins"); - } - } - }); - } - } -}); 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 b/app/assets/javascripts/admin/routes/admin-reports-show.js new file mode 100644 index 0000000000..3a8a5430a9 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-reports-show.js @@ -0,0 +1,69 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + queryParams: { + start_date: { refreshModel: true }, + end_date: { refreshModel: true }, + filters: { refreshModel: true } + }, + + model(params) { + params.customFilters = params.filters; + delete params.filters; + + params.startDate = + params.start_date || + moment() + .subtract(1, "day") + .subtract(1, "month") + .startOf("day") + .format("YYYY-MM-DD"); + delete params.start_date; + + params.endDate = + params.end_date || + moment() + .endOf("day") + .format("YYYY-MM-DD"); + delete params.end_date; + + return params; + }, + + deserializeQueryParam(value, urlKey, defaultValueType) { + if (urlKey === "filters") { + return JSON.parse(decodeURIComponent(value)); + } + + return this._super(value, urlKey, defaultValueType); + }, + + serializeQueryParam(value, urlKey, defaultValueType) { + if (urlKey === "filters") { + if (value && Object.keys(value).length > 0) { + return JSON.stringify(value); + } else { + return null; + } + } + + return this._super(value, urlKey, defaultValueType); + }, + + actions: { + onParamsChange(params) { + const queryParams = { + type: params.type, + start_date: params.startDate + ? params.startDate.toISOString(true).split("T")[0] + : null, + filters: params.filters, + end_date: params.endDate + ? params.endDate.toISOString(true).split("T")[0] + : null + }; + + this.transitionTo("adminReports.show", { queryParams }); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-reports-show.js.es6 b/app/assets/javascripts/admin/routes/admin-reports-show.js.es6 deleted file mode 100644 index 1fa8bc739d..0000000000 --- a/app/assets/javascripts/admin/routes/admin-reports-show.js.es6 +++ /dev/null @@ -1,67 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; - -export default DiscourseRoute.extend({ - queryParams: { - start_date: { refreshModel: true }, - end_date: { refreshModel: true }, - filters: { refreshModel: true } - }, - - model(params) { - params.customFilters = params.filters; - delete params.filters; - - params.startDate = - params.start_date || - moment - .utc() - .subtract(1, "day") - .subtract(1, "month") - .startOf("day") - .format("YYYY-MM-DD"); - delete params.start_date; - - params.endDate = - params.end_date || - moment - .utc() - .endOf("day") - .format("YYYY-MM-DD"); - delete params.end_date; - - return params; - }, - - deserializeQueryParam(value, urlKey, defaultValueType) { - if (urlKey === "filters") { - return JSON.parse(decodeURIComponent(value)); - } - - return this._super(value, urlKey, defaultValueType); - }, - - serializeQueryParam(value, urlKey, defaultValueType) { - if (urlKey === "filters") { - if (value && Object.keys(value).length > 0) { - return JSON.stringify(value); - } else { - return null; - } - } - - return this._super(value, urlKey, defaultValueType); - }, - - actions: { - onParamsChange(params) { - const queryParams = { - type: params.type, - start_date: params.startDate, - filters: params.filters, - end_date: params.endDate - }; - - this.transitionTo("adminReports.show", { queryParams }); - } - } -}); 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 b/app/assets/javascripts/admin/routes/admin-site-settings-category.js new file mode 100644 index 0000000000..e8a9b853d4 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-site-settings-category.js @@ -0,0 +1,24 @@ +import I18n from "I18n"; +import EmberObject from "@ember/object"; +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + model(params) { + // The model depends on user input, so let the controller do the work: + this.controllerFor("adminSiteSettingsCategory").set( + "categoryNameKey", + params.category_id + ); + this.controllerFor("adminSiteSettings").set( + "categoryNameKey", + params.category_id + ); + return EmberObject.create({ + nameKey: params.category_id, + name: I18n.t("admin.site_settings.categories." + params.category_id), + siteSettings: this.controllerFor("adminSiteSettingsCategory").get( + "filteredContent" + ) + }); + } +}); 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.es6 deleted file mode 100644 index b5a2f810b2..0000000000 --- a/app/assets/javascripts/admin/routes/admin-site-settings-category.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -import EmberObject from "@ember/object"; -import DiscourseRoute from "discourse/routes/discourse"; - -export default DiscourseRoute.extend({ - model(params) { - // The model depends on user input, so let the controller do the work: - this.controllerFor("adminSiteSettingsCategory").set( - "categoryNameKey", - params.category_id - ); - this.controllerFor("adminSiteSettings").set( - "categoryNameKey", - params.category_id - ); - return EmberObject.create({ - nameKey: params.category_id, - name: I18n.t("admin.site_settings.categories." + params.category_id), - siteSettings: this.controllerFor("adminSiteSettingsCategory").get( - "filteredContent" - ) - }); - } -}); 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 b/app/assets/javascripts/admin/routes/admin-users-list-show.js new file mode 100644 index 0000000000..c16591d37f --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-users-list-show.js @@ -0,0 +1,30 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + queryParams: { + order: { refreshModel: true }, + asc: { refreshModel: true } + }, + + // TODO: this has been introduced to fix a bug in admin-users-list-show + // loading AdminUser model multiple times without refactoring the controller + beforeModel(transition) { + const routeName = "adminUsersList.show"; + + if (transition.targetName === routeName) { + const params = transition.routeInfos.find(a => a.name === routeName) + .params; + const controller = this.controllerFor(routeName); + if (controller) { + controller.setProperties({ + order: transition.to.queryParams.order, + asc: transition.to.queryParams.asc, + query: params.filter, + refreshing: false + }); + + controller.resetFilters(); + } + } + } +}); 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.es6 deleted file mode 100644 index 764fe4ad34..0000000000 --- a/app/assets/javascripts/admin/routes/admin-users-list-show.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; - -export default DiscourseRoute.extend({ - queryParams: { - order: { refreshModel: true }, - ascending: { refreshModel: true } - }, - - // TODO: this has been introduced to fix a bug in admin-users-list-show - // loading AdminUser model multiple times without refactoring the controller - beforeModel(transition) { - const routeName = "adminUsersList.show"; - - if (transition.targetName === routeName) { - const params = transition.routeInfos.find(a => a.name === routeName) - .params; - const controller = this.controllerFor(routeName); - if (controller) { - controller.setProperties({ - order: transition.to.queryParams.order, - ascending: transition.to.queryParams.ascending, - query: params.filter, - refreshing: false - }); - - controller.resetFilters(); - } - } - } -}); 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 b/app/assets/javascripts/admin/routes/admin-watched-words-action.js new file mode 100644 index 0000000000..af416c48de --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-watched-words-action.js @@ -0,0 +1,20 @@ +import I18n from "I18n"; +import EmberObject from "@ember/object"; +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + model(params) { + this.controllerFor("adminWatchedWordsAction").set( + "actionNameKey", + params.action_id + ); + let filteredContent = this.controllerFor("adminWatchedWordsAction").get( + "filteredContent" + ); + return EmberObject.create({ + nameKey: params.action_id, + name: I18n.t("admin.watched_words.actions." + params.action_id), + words: filteredContent + }); + } +}); 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.es6 deleted file mode 100644 index 358cfebe67..0000000000 --- a/app/assets/javascripts/admin/routes/admin-watched-words-action.js.es6 +++ /dev/null @@ -1,19 +0,0 @@ -import EmberObject from "@ember/object"; -import DiscourseRoute from "discourse/routes/discourse"; - -export default DiscourseRoute.extend({ - model(params) { - this.controllerFor("adminWatchedWordsAction").set( - "actionNameKey", - params.action_id - ); - let filteredContent = this.controllerFor("adminWatchedWordsAction").get( - "filteredContent" - ); - return EmberObject.create({ - nameKey: params.action_id, - name: I18n.t("admin.watched_words.actions." + params.action_id), - words: filteredContent - }); - } -}); 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 b/app/assets/javascripts/admin/routes/admin-web-hooks-show.js new file mode 100644 index 0000000000..f0860e79f7 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-web-hooks-show.js @@ -0,0 +1,30 @@ +import { get } from "@ember/object"; +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + serialize(model) { + return { web_hook_id: model.get("id") || "new" }; + }, + + model(params) { + if (params.web_hook_id === "new") { + return this.store.createRecord("web-hook"); + } + return this.store.find("web-hook", get(params, "web_hook_id")); + }, + + setupController(controller, model) { + if (model.get("isNew")) { + model.set("web_hook_event_types", controller.get("defaultEventTypes")); + } + + model.set("category_ids", model.get("category_ids")); + model.set("tag_names", model.get("tag_names")); + model.set("group_ids", model.get("group_ids")); + controller.setProperties({ model, saved: false }); + }, + + renderTemplate() { + this.render("admin/templates/web-hooks-show", { into: "adminApi" }); + } +}); 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.es6 deleted file mode 100644 index 34aed775cc..0000000000 --- a/app/assets/javascripts/admin/routes/admin-web-hooks-show.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -import { get } from "@ember/object"; -import { isEmpty } from "@ember/utils"; -import DiscourseRoute from "discourse/routes/discourse"; - -export default DiscourseRoute.extend({ - serialize(model) { - return { web_hook_id: model.get("id") || "new" }; - }, - - model(params) { - if (params.web_hook_id === "new") { - return this.store.createRecord("web-hook"); - } - return this.store.find("web-hook", get(params, "web_hook_id")); - }, - - setupController(controller, model) { - if (model.get("isNew") || isEmpty(model.get("web_hook_event_types"))) { - model.set("web_hook_event_types", controller.get("defaultEventTypes")); - } - - model.set("category_ids", model.get("category_ids")); - model.set("tag_names", model.get("tag_names")); - model.set("group_ids", model.get("group_ids")); - controller.setProperties({ model, saved: false }); - }, - - renderTemplate() { - this.render("admin/templates/web-hooks-show", { into: "adminApi" }); - } -}); 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 b/app/assets/javascripts/admin/routes/admin.js new file mode 100644 index 0000000000..0a7ae99fdd --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin.js @@ -0,0 +1,19 @@ +import I18n from "I18n"; +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + titleToken() { + return I18n.t("admin_title"); + }, + + activate() { + this.controllerFor("application").setProperties({ + showTop: false, + showFooter: false + }); + }, + + deactivate() { + this.controllerFor("application").set("showTop", true); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin.js.es6 b/app/assets/javascripts/admin/routes/admin.js.es6 deleted file mode 100644 index bc4e8173ef..0000000000 --- a/app/assets/javascripts/admin/routes/admin.js.es6 +++ /dev/null @@ -1,18 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; - -export default DiscourseRoute.extend({ - titleToken() { - return I18n.t("admin_title"); - }, - - activate() { - this.controllerFor("application").setProperties({ - showTop: false, - showFooter: false - }); - }, - - deactivate() { - this.controllerFor("application").set("showTop", true); - } -}); diff --git a/app/assets/javascripts/admin/services/admin-tools.js b/app/assets/javascripts/admin/services/admin-tools.js new file mode 100644 index 0000000000..6193beed66 --- /dev/null +++ b/app/assets/javascripts/admin/services/admin-tools.js @@ -0,0 +1,142 @@ +import I18n from "I18n"; +// A service that can act as a bridge between the front end Discourse application +// and the admin application. Use this if you need front end code to access admin +// modules. Inject it optionally, and if it exists go to town! + +import EmberObject from "@ember/object"; +import AdminUser from "admin/models/admin-user"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { ajax } from "discourse/lib/ajax"; +import showModal from "discourse/lib/show-modal"; +import { getOwner } from "discourse-common/lib/get-owner"; +import Service from "@ember/service"; +import { Promise } from "rsvp"; + +export default Service.extend({ + init() { + this._super(...arguments); + + // TODO: Make `siteSettings` a service that can be injected + this.siteSettings = getOwner(this).lookup("site-settings:main"); + }, + + showActionLogs(target, filters) { + const controller = getOwner(target).lookup( + "controller:adminLogs.staffActionLogs" + ); + target.transitionToRoute("adminLogs.staffActionLogs").then(() => { + controller.set("filters", EmberObject.create()); + controller._changeFilters(filters); + }); + }, + + checkSpammer(userId) { + return AdminUser.find(userId).then(au => this.spammerDetails(au)); + }, + + deleteUser(id) { + AdminUser.find(id).then(user => user.destroy({ deletePosts: true })); + }, + + spammerDetails(adminUser) { + return { + deleteUser: () => this._deleteSpammer(adminUser), + canDelete: + adminUser.get("can_be_deleted") && adminUser.get("can_delete_all_posts") + }; + }, + + _showControlModal(type, user, opts) { + opts = opts || {}; + + let controller = showModal(`admin-${type}-user`, { + admin: true, + modalClass: `${type}-user-modal` + }); + controller.setProperties({ postId: opts.postId, postEdit: opts.postEdit }); + + return (user.adminUserView + ? Promise.resolve(user) + : AdminUser.find(user.get("id")) + ).then(loadedUser => { + controller.setProperties({ + user: loadedUser, + loadingUser: false, + before: opts.before, + successCallback: opts.successCallback + }); + }); + }, + + showSilenceModal(user, opts) { + this._showControlModal("silence", user, opts); + }, + + showSuspendModal(user, opts) { + this._showControlModal("suspend", user, opts); + }, + + _deleteSpammer(adminUser) { + // Try loading the email if the site supports it + let tryEmail = this.siteSettings.moderators_view_emails + ? adminUser.checkEmail() + : Promise.resolve(); + + return tryEmail.then(() => { + let message = I18n.messageFormat("flagging.delete_confirm_MF", { + POSTS: adminUser.get("post_count"), + TOPICS: adminUser.get("topic_count"), + email: + adminUser.get("email") || I18n.t("flagging.hidden_email_address"), + ip_address: + adminUser.get("ip_address") || I18n.t("flagging.ip_address_missing") + }); + + let userId = adminUser.get("id"); + + return new Promise((resolve, reject) => { + const buttons = [ + { + label: I18n.t("composer.cancel"), + class: "d-modal-cancel", + link: true + }, + { + label: + `${iconHTML("exclamation-triangle")} ` + + I18n.t("flagging.yes_delete_spammer"), + class: "btn btn-danger confirm-delete", + callback() { + return ajax(`/admin/users/${userId}.json`, { + type: "DELETE", + data: { + delete_posts: true, + block_email: true, + block_urls: true, + block_ip: true, + delete_as_spammer: true, + context: window.location.pathname + } + }) + .then(result => { + if (result.deleted) { + resolve(); + } else { + throw new Error("failed to delete"); + } + }) + .catch(() => { + bootbox.alert(I18n.t("admin.user.delete_failed")); + reject(); + }); + } + } + ]; + + bootbox.dialog(message, buttons, { + classes: "flagging-delete-spammer" + }); + }); + }); + } +}); diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6 deleted file mode 100644 index bb5e1ac06a..0000000000 --- a/app/assets/javascripts/admin/services/admin-tools.js.es6 +++ /dev/null @@ -1,141 +0,0 @@ -// A service that can act as a bridge between the front end Discourse application -// and the admin application. Use this if you need front end code to access admin -// modules. Inject it optionally, and if it exists go to town! - -import EmberObject from "@ember/object"; -import AdminUser from "admin/models/admin-user"; -import { iconHTML } from "discourse-common/lib/icon-library"; -import { ajax } from "discourse/lib/ajax"; -import showModal from "discourse/lib/show-modal"; -import { getOwner } from "discourse-common/lib/get-owner"; -import Service from "@ember/service"; -import { Promise } from "rsvp"; - -export default Service.extend({ - init() { - this._super(...arguments); - - // TODO: Make `siteSettings` a service that can be injected - this.siteSettings = getOwner(this).lookup("site-settings:main"); - }, - - showActionLogs(target, filters) { - const controller = getOwner(target).lookup( - "controller:adminLogs.staffActionLogs" - ); - target.transitionToRoute("adminLogs.staffActionLogs").then(() => { - controller.set("filters", EmberObject.create()); - controller._changeFilters(filters); - }); - }, - - checkSpammer(userId) { - return AdminUser.find(userId).then(au => this.spammerDetails(au)); - }, - - deleteUser(id) { - AdminUser.find(id).then(user => user.destroy({ deletePosts: true })); - }, - - spammerDetails(adminUser) { - return { - deleteUser: () => this._deleteSpammer(adminUser), - canDelete: - adminUser.get("can_be_deleted") && adminUser.get("can_delete_all_posts") - }; - }, - - _showControlModal(type, user, opts) { - opts = opts || {}; - - let controller = showModal(`admin-${type}-user`, { - admin: true, - modalClass: `${type}-user-modal` - }); - controller.setProperties({ postId: opts.postId, postEdit: opts.postEdit }); - - return (user.adminUserView - ? Promise.resolve(user) - : AdminUser.find(user.get("id")) - ).then(loadedUser => { - controller.setProperties({ - user: loadedUser, - loadingUser: false, - before: opts.before, - successCallback: opts.successCallback - }); - }); - }, - - showSilenceModal(user, opts) { - this._showControlModal("silence", user, opts); - }, - - showSuspendModal(user, opts) { - this._showControlModal("suspend", user, opts); - }, - - _deleteSpammer(adminUser) { - // Try loading the email if the site supports it - let tryEmail = this.siteSettings.moderators_view_emails - ? adminUser.checkEmail() - : Promise.resolve(); - - return tryEmail.then(() => { - let message = I18n.messageFormat("flagging.delete_confirm_MF", { - POSTS: adminUser.get("post_count"), - TOPICS: adminUser.get("topic_count"), - email: - adminUser.get("email") || I18n.t("flagging.hidden_email_address"), - ip_address: - adminUser.get("ip_address") || I18n.t("flagging.ip_address_missing") - }); - - let userId = adminUser.get("id"); - - return new Promise((resolve, reject) => { - const buttons = [ - { - label: I18n.t("composer.cancel"), - class: "d-modal-cancel", - link: true - }, - { - label: - `${iconHTML("exclamation-triangle")} ` + - I18n.t("flagging.yes_delete_spammer"), - class: "btn btn-danger confirm-delete", - callback() { - return ajax(`/admin/users/${userId}.json`, { - type: "DELETE", - data: { - delete_posts: true, - block_email: true, - block_urls: true, - block_ip: true, - delete_as_spammer: true, - context: window.location.pathname - } - }) - .then(result => { - if (result.deleted) { - resolve(); - } else { - throw new Error("failed to delete"); - } - }) - .catch(() => { - bootbox.alert(I18n.t("admin.user.delete_failed")); - reject(); - }); - } - } - ]; - - bootbox.dialog(message, buttons, { - classes: "flagging-delete-spammer" - }); - }); - }); - } -}); 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 @@ -
+