Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Brennan
819efb0434
Revert "DEV: enables threadsafe for system tests"
This reverts commit b8100ad1ae.

This was causing many issues in CI, including AR connections
not being expired because of threading issues, which was also
causing rails_helper to raise an error because we had busy
connections at the end of spec runs.

```
Cannot expire connection, it is owned by a different thread:
/__w/discourse/discourse/vendor/bundle/ruby/3.1.0/gems/puma-6.0.1/lib/puma/thread_pool.rb:106
sleep_forever>. Current thread: #<Thread:0x00007f541966fcc8 run>.
```

And:

```
Failure/Error: raise ActiveRecord::Base.connection_pool.stat.inspect

RuntimeError:
  {:size=>5, :connections=>2, :busy=>2, :dead=>0, :idle=>0, :waiting=>0, :checkout_timeout=>5.0}
```

See https://github.com/discourse/discourse/actions/runs/3825882529/jobs/6509204143
for examples.
2023-01-03 11:38:51 +10:00
5271 changed files with 143514 additions and 202996 deletions

View File

@ -12,6 +12,3 @@ indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.hbs]
insert_final_newline = false

View File

@ -12,4 +12,3 @@ node_modules/
spec/ spec/
dist/ dist/
tmp/ tmp/
documentation/

View File

@ -58,16 +58,3 @@ bbe5d8d5cf1220165842985c0e2cd4c454d501cd
# DEV: Template colocation for sidebar files # DEV: Template colocation for sidebar files
95c7cdab941a56686ac5831d2a5c5eca38d780c5 95c7cdab941a56686ac5831d2a5c5eca38d780c5
# DEV: Apply prettier to hbs files
c8e2e37fa77d3c3c69c7572866017e9bb92befa3
# DEV: Apply syntax_tree to...
5a003715d366e1d871f9fcb0656dc9e23e9c2259
64171730827c58df26a7ad75f0e58f17c2add118
b0fda61a8e75c81e3458c8af9d2afe9d32183457
cb932d6ee1b3b3571e4d4d9118635e2dbf58f0ef
0cf6421716d0908da57ad7743a2decb08588b48a
7c77cc6a580d7cb49f8c19ceee8cfdd08862259d
436b3b392b9c917510d4ff0d73a5167cd3eb936c
055310cea496519a996b9c3bf4dc7e716cfe62ba

View File

@ -22,9 +22,6 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Set working directory owner
run: chown root:root .
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 fetch-depth: 1
@ -71,12 +68,6 @@ jobs:
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
run: bundle exec rubocop --parallel . run: bundle exec rubocop --parallel .
- name: syntax_tree
if: ${{ !cancelled() }}
run: |
set -E
bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake')
- name: ESLint (core) - name: ESLint (core)
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
run: yarn eslint app/assets/javascripts run: yarn eslint app/assets/javascripts
@ -92,10 +83,8 @@ jobs:
yarn pprettier --list-different \ yarn pprettier --list-different \
"app/assets/stylesheets/**/*.scss" \ "app/assets/stylesheets/**/*.scss" \
"app/assets/javascripts/**/*.js" \ "app/assets/javascripts/**/*.js" \
"app/assets/javascripts/**/*.hbs" \
"plugins/**/assets/stylesheets/**/*.scss" \ "plugins/**/assets/stylesheets/**/*.scss" \
"plugins/**/assets/javascripts/**/*.js" \ "plugins/**/assets/javascripts/**/*.js"
"plugins/**/assets/javascripts/**/*.hbs" \
- name: Ember template lint - name: Ember template lint
if: ${{ !cancelled() }} if: ${{ !cancelled() }}

View File

@ -18,9 +18,9 @@ permissions:
jobs: jobs:
build: build:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')" if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
name: ${{ matrix.target }} ${{ matrix.build_type }} ${{ matrix.ruby }} name: ${{ matrix.target }} ${{ matrix.build_type }}
runs-on: ${{ (matrix.build_type == 'annotations') && 'ubuntu-latest' || 'ubuntu-20.04-8core' }} runs-on: ${{ (matrix.build_type == 'annotations') && 'ubuntu-latest' || 'ubuntu-20.04-8core' }}
container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }}${{ (matrix.ruby == '3.1') && '-ruby-3.1.0' || '' }} container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }}
timeout-minutes: 20 timeout-minutes: 20
env: env:
@ -29,8 +29,7 @@ jobs:
RAILS_ENV: test RAILS_ENV: test
PGUSER: discourse PGUSER: discourse
PGPASSWORD: discourse PGPASSWORD: discourse
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' || matrix.build_type == 'system' }} USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' }}
CAPBYARA_DEFAULT_MAX_WAIT_TIME: 4
strategy: strategy:
fail-fast: false fail-fast: false
@ -38,7 +37,6 @@ jobs:
matrix: matrix:
build_type: [backend, frontend, system, annotations] build_type: [backend, frontend, system, annotations]
target: [core, plugins] target: [core, plugins]
ruby: ['3.2']
exclude: exclude:
- build_type: annotations - build_type: annotations
target: plugins target: plugins
@ -46,9 +44,6 @@ jobs:
target: core # Handled by core_frontend_tests job (below) target: core # Handled by core_frontend_tests job (below)
steps: steps:
- name: Set working directory owner
run: chown root:root .
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 fetch-depth: 1
@ -72,9 +67,9 @@ jobs:
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ matrix.ruby }}-gem- ${{ runner.os }}-gem-
- name: Setup gems - name: Setup gems
run: | run: |
@ -159,22 +154,6 @@ jobs:
path: tmp/turbo_rspec_runtime.log path: tmp/turbo_rspec_runtime.log
key: rspec-runtime-backend-core key: rspec-runtime-backend-core
- name: Run Zeitwerk check
if: matrix.build_type == 'backend'
env:
LOAD_PLUGINS: ${{ (matrix.target == 'plugins') && '1' || '0' }}
run: |
if ! bin/rails zeitwerk:check --trace; then
echo
echo "---------------------------------------------"
echo
echo "::error::'bin/rails zeitwerk:check' failed - the app will fail to boot with 'eager_load=true' (e.g. in production)."
echo "To reproduce locally, run 'bin/rails zeitwerk:check'."
echo "Alternatively, you can run your local server/tests with the 'DISCOURSE_ZEITWERK_EAGER_LOAD=1' environment variable."
echo
exit 1
fi
- name: Core RSpec - name: Core RSpec
if: matrix.build_type == 'backend' && matrix.target == 'core' if: matrix.build_type == 'backend' && matrix.target == 'core'
run: bin/turbo_rspec --verbose run: bin/turbo_rspec --verbose
@ -192,17 +171,13 @@ jobs:
if: matrix.build_type == 'system' if: matrix.build_type == 'system'
run: bin/ember-cli --build run: bin/ember-cli --build
- name: Setup Webdriver
if: matrix.build_type == 'system'
run: bin/rails runner "require 'webdrivers'; Webdrivers::Chromedriver.update"
- name: Core System Tests - name: Core System Tests
if: matrix.build_type == 'system' && matrix.target == 'core' if: matrix.build_type == 'system' && matrix.target == 'core'
run: bin/rspec spec/system run: bin/rspec spec/system --format documentation --profile
- name: Plugin System Tests - name: Plugin System Tests
if: matrix.build_type == 'system' && matrix.target == 'plugins' if: matrix.build_type == 'system' && matrix.target == 'plugins'
run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system --format documentation --profile
- name: Upload failed system test screenshots - name: Upload failed system test screenshots
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

21
.jsdoc
View File

@ -1,21 +0,0 @@
// jsdoc doesn't accept paths starting with _ (which is the case on github runners)
// so we need to alter the default config
{
"source": {
"excludePattern": ""
},
"templates": {
"default": {
"includeDate": false
}
},
"opts": {
"template": "./node_modules/tidy-jsdoc",
"prism-theme": "prism-custom",
"encoding": "utf8",
"recurse": true
},
"metadata": {
"title": "Discourse"
}
}

View File

@ -15,7 +15,6 @@ ignored:
bundler: bundler:
- cgi # Ruby (default gem) - cgi # Ruby (default gem)
- date # Ruby (default gem) - date # Ruby (default gem)
- digest # Ruby (default gem)
- io-wait # Ruby (default gem) - io-wait # Ruby (default gem)
- json # Ruby (default gem) - json # Ruby (default gem)
- net-http # Ruby (default gem) - net-http # Ruby (default gem)

View File

@ -11,8 +11,7 @@
"packages": { "packages": {
"@fortawesome/fontawesome-free": "*", "@fortawesome/fontawesome-free": "*",
"ember-template-lint-plugin-discourse": "*", "ember-template-lint-plugin-discourse": "*",
"squoosh": "2.0.0", "squoosh": "2.0.0"
"taffydb": "2.6.2"
}, },
"corrections": true "corrections": true
} }

View File

@ -3,7 +3,6 @@ plugins/**/assets/stylesheets/vendor/
plugins/**/assets/javascripts/vendor/ plugins/**/assets/javascripts/vendor/
plugins/**/config/locales/**/*.yml plugins/**/config/locales/**/*.yml
plugins/**/config/*.yml plugins/**/config/*.yml
documentation/
package.json package.json
config/locales/**/*.yml config/locales/**/*.yml
!config/locales/**/*.en*.yml !config/locales/**/*.en*.yml

View File

@ -1,5 +1,5 @@
inherit_gem: inherit_gem:
rubocop-discourse: stree-compat.yml rubocop-discourse: default.yml
# Still work to do in ensuring we don't link old files # Still work to do in ensuring we don't link old files
Discourse/NoAddReferenceOrAliasesActiveRecordMigration: Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
@ -7,7 +7,3 @@ Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
Discourse/NoResetColumnInformationInMigrations: Discourse/NoResetColumnInformationInMigrations:
Enabled: true Enabled: true
Lint/Debugger:
Exclude:
- script/**/*

View File

@ -1 +1 @@
3.2.1 3.1.3

View File

@ -1,2 +1,2 @@
--print-width=100 --print-width=100
--plugins=plugin/trailing_comma,disable_ternary --plugins=plugin/trailing_comma

View File

@ -15,7 +15,7 @@ module.exports = {
"directory-item-value", "directory-item-value",
"directory-table-header-title", "directory-table-header-title",
"loading-spinner", "loading-spinner",
"directory-item-label", "mobile-directory-item-label",
], ],
}, },
"no-implicit-this": { "no-implicit-this": {

297
Gemfile
View File

@ -1,51 +1,51 @@
# frozen_string_literal: true # frozen_string_literal: true
source "https://rubygems.org" source 'https://rubygems.org'
# if there is a super emergency and rubygems is playing up, try # if there is a super emergency and rubygems is playing up, try
#source 'http://production.cf.rubygems.org' #source 'http://production.cf.rubygems.org'
gem "bootsnap", require: false, platform: :mri gem 'bootsnap', require: false, platform: :mri
def rails_master? def rails_master?
ENV["RAILS_MASTER"] == "1" ENV["RAILS_MASTER"] == '1'
end end
if rails_master? if rails_master?
gem "arel", git: "https://github.com/rails/arel.git" gem 'arel', git: 'https://github.com/rails/arel.git'
gem "rails", git: "https://github.com/rails/rails.git" gem 'rails', git: 'https://github.com/rails/rails.git'
else else
# NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit # NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit
# this allows us to include the bits of rails we use without pieces we do not. # 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 # To issue a rails update bump the version number here
rails_version = "7.0.4.3" rails_version = '7.0.3.1'
gem "actionmailer", rails_version gem 'actionmailer', rails_version
gem "actionpack", rails_version gem 'actionpack', rails_version
gem "actionview", rails_version gem 'actionview', rails_version
gem "activemodel", rails_version gem 'activemodel', rails_version
gem "activerecord", rails_version gem 'activerecord', rails_version
gem "activesupport", rails_version gem 'activesupport', rails_version
gem "railties", rails_version gem 'railties', rails_version
gem "sprockets-rails" gem 'sprockets-rails'
end end
gem "json" gem 'json'
# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals # TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
# We intend to drop sprockets rather than upgrade to 4.x # This is a desired upgrade we should get to.
gem "sprockets", git: "https://github.com/rails/sprockets", branch: "3.x" gem 'sprockets', '3.7.2'
# this will eventually be added to rails, # this will eventually be added to rails,
# allows us to precompile all our templates in the unicorn master # allows us to precompile all our templates in the unicorn master
gem "actionview_precompiler", require: false gem 'actionview_precompiler', require: false
gem "discourse-seed-fu" gem 'discourse-seed-fu'
gem "mail", git: "https://github.com/discourse/mail.git" gem 'mail', git: 'https://github.com/discourse/mail.git'
gem "mini_mime" gem 'mini_mime'
gem "mini_suffix" gem 'mini_suffix'
gem "redis" gem 'redis'
# This is explicitly used by Sidekiq and is an optional dependency. # This is explicitly used by Sidekiq and is an optional dependency.
# We tell Sidekiq to use the namespace "sidekiq" which triggers this # We tell Sidekiq to use the namespace "sidekiq" which triggers this
@ -53,79 +53,79 @@ gem "redis"
# redis namespace support is optional # redis namespace support is optional
# We already namespace stuff in DiscourseRedis, so we should consider # We already namespace stuff in DiscourseRedis, so we should consider
# just using a single implementation in core vs having 2 namespace implementations # just using a single implementation in core vs having 2 namespace implementations
gem "redis-namespace" gem 'redis-namespace'
# NOTE: AM serializer gets a lot slower with recent updates # NOTE: AM serializer gets a lot slower with recent updates
# we used an old branch which is the fastest one out there # we used an old branch which is the fastest one out there
# are long term goal here is to fork this gem so we have a # are long term goal here is to fork this gem so we have a
# better maintained living fork # better maintained living fork
gem "active_model_serializers", "~> 0.8.3" gem 'active_model_serializers', '~> 0.8.3'
gem "http_accept_language", require: false gem 'http_accept_language', require: false
gem "discourse-fonts", require: "discourse_fonts" gem 'discourse-fonts', require: 'discourse_fonts'
gem "message_bus" gem 'message_bus'
gem "rails_multisite" gem 'rails_multisite'
gem "fast_xs", platform: :ruby gem 'fast_xs', platform: :ruby
gem "xorcist" gem 'xorcist'
gem "fastimage" gem 'fastimage'
gem "aws-sdk-s3", require: false gem 'aws-sdk-s3', require: false
gem "aws-sdk-sns", require: false gem 'aws-sdk-sns', require: false
gem "excon", require: false gem 'excon', require: false
gem "unf", require: false gem 'unf', require: false
gem "email_reply_trimmer" gem 'email_reply_trimmer'
gem "image_optim" gem 'image_optim'
gem "multi_json" gem 'multi_json'
gem "mustache" gem 'mustache'
gem "nokogiri" gem 'nokogiri'
gem "loofah" gem 'loofah'
gem "css_parser", require: false gem 'css_parser', require: false
gem "omniauth" gem 'omniauth'
gem "omniauth-facebook" gem 'omniauth-facebook'
gem "omniauth-twitter" gem 'omniauth-twitter'
gem "omniauth-github" gem 'omniauth-github'
gem "omniauth-oauth2", require: false gem 'omniauth-oauth2', require: false
gem "omniauth-google-oauth2" gem 'omniauth-google-oauth2'
# pending: https://github.com/ohler55/oj/issues/789 # pending: https://github.com/ohler55/oj/issues/789
gem "oj", "3.13.14" gem 'oj', '3.13.14'
gem "pg" gem 'pg'
gem "mini_sql" gem 'mini_sql'
gem "pry-rails", require: false gem 'pry-rails', require: false
gem "pry-byebug", require: false gem 'pry-byebug', require: false
gem "rtlcss", require: false gem 'r2', require: false
gem "rake" gem 'rake'
gem "thor", require: false gem 'thor', require: false
gem "diffy", require: false gem 'diffy', require: false
gem "rinku" gem 'rinku'
gem "sidekiq" gem 'sidekiq'
gem "mini_scheduler" gem 'mini_scheduler'
gem "execjs", require: false gem 'execjs', require: false
gem "mini_racer" gem 'mini_racer'
gem "highline", require: false gem 'highline', require: false
gem "rack" gem 'rack'
gem "rack-protection" # security gem 'rack-protection' # security
gem "cbor", require: false gem 'cbor', require: false
gem "cose", require: false gem 'cose', require: false
gem "addressable" gem 'addressable'
gem "json_schemer" gem 'json_schemer'
gem "net-smtp", require: false gem "net-smtp", require: false
gem "net-imap", require: false gem "net-imap", require: false
@ -135,149 +135,146 @@ gem "digest", require: false
# Gems used only for assets and not required in production environments by default. # Gems used only for assets and not required in production environments by default.
# Allow everywhere for now cause we are allowing asset debugging in production # Allow everywhere for now cause we are allowing asset debugging in production
group :assets do group :assets do
gem "uglifier" gem 'uglifier'
end end
group :test do group :test do
gem "capybara", require: false gem 'capybara', require: false
gem "webmock", require: false gem 'webmock', require: false
gem "fakeweb", require: false gem 'fakeweb', require: false
gem "minitest", require: false gem 'minitest', require: false
gem "simplecov", require: false gem 'simplecov', require: false
gem "selenium-webdriver", require: false gem 'selenium-webdriver', require: false
gem "test-prof" gem "test-prof"
gem "webdrivers", require: false gem 'webdrivers', require: false
end end
group :test, :development do group :test, :development do
gem "rspec" gem 'rspec'
gem "listen", require: false gem 'listen', require: false
gem "certified", require: false gem 'certified', require: false
gem "fabrication", require: false gem 'fabrication', require: false
gem "mocha", require: false gem 'mocha', require: false
gem "rb-fsevent", require: RUBY_PLATFORM =~ /darwin/i ? "rb-fsevent" : false gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
gem "rspec-rails" gem 'rspec-rails'
gem "shoulda-matchers", require: false gem 'shoulda-matchers', require: false
gem "rspec-html-matchers" gem 'rspec-html-matchers'
gem "byebug", require: ENV["RM_INFO"].nil?, platform: :mri gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri
gem "rubocop-discourse", require: false gem 'rubocop-discourse', require: false
gem "parallel_tests" gem 'parallel_tests'
gem "rswag-specs" gem 'rswag-specs'
gem "annotate" gem 'annotate'
gem "syntax_tree"
gem "syntax_tree-disable_ternary"
end end
group :development do group :development do
gem "ruby-prof", require: false, platform: :mri gem 'ruby-prof', require: false, platform: :mri
gem "bullet", require: !!ENV["BULLET"] gem 'bullet', require: !!ENV['BULLET']
gem "better_errors", platform: :mri, require: !!ENV["BETTER_ERRORS"] gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS']
gem "binding_of_caller" gem 'binding_of_caller'
gem "yaml-lint" gem 'yaml-lint'
gem "yard"
end end
if ENV["ALLOW_DEV_POPULATE"] == "1" if ENV["ALLOW_DEV_POPULATE"] == "1"
gem "discourse_dev_assets" gem 'discourse_dev_assets'
gem "faker", "~> 2.16" gem 'faker', "~> 2.16"
else else
group :development, :test do group :development, :test do
gem "discourse_dev_assets" gem 'discourse_dev_assets'
gem "faker", "~> 2.16" gem 'faker', "~> 2.16"
end end
end end
# this is an optional gem, it provides a high performance replacement # this is an optional gem, it provides a high performance replacement
# to String#blank? a method that is called quite frequently in current # to String#blank? a method that is called quite frequently in current
# ActiveRecord, this may change in the future # ActiveRecord, this may change in the future
gem "fast_blank", platform: :ruby gem 'fast_blank', platform: :ruby
# this provides a very efficient lru cache # this provides a very efficient lru cache
gem "lru_redux" gem 'lru_redux'
gem "htmlentities", require: false gem 'htmlentities', require: false
# IMPORTANT: mini profiler monkey patches, so it better be required last # IMPORTANT: mini profiler monkey patches, so it better be required last
# If you want to amend mini profiler to do the monkey patches in the railties # If you want to amend mini profiler to do the monkey patches in the railties
# we are open to it. by deferring require to the initializer we can configure discourse installs without it # we are open to it. by deferring require to the initializer we can configure discourse installs without it
gem "rack-mini-profiler", require: ["enable_rails_patches"] gem 'rack-mini-profiler', require: ['enable_rails_patches']
gem "unicorn", require: false, platform: :ruby gem 'unicorn', require: false, platform: :ruby
gem "puma", require: false gem 'puma', require: false
gem "rbtrace", require: false, platform: :mri gem 'rbtrace', require: false, platform: :mri
gem "gc_tracer", require: false, platform: :mri gem 'gc_tracer', require: false, platform: :mri
# required for feed importing and embedding # required for feed importing and embedding
gem "ruby-readability", require: false gem 'ruby-readability', require: false
# rss gem is a bundled gem from Ruby 3 onwards # rss gem is a bundled gem from Ruby 3 onwards
gem "rss", require: false gem 'rss', require: false
gem "stackprof", require: false, platform: :mri gem 'stackprof', require: false, platform: :mri
gem "memory_profiler", require: false, platform: :mri gem 'memory_profiler', require: false, platform: :mri
gem "cppjieba_rb", require: false gem 'cppjieba_rb', require: false
gem "lograge", require: false gem 'lograge', require: false
gem "logstash-event", require: false gem 'logstash-event', require: false
gem "logstash-logger", require: false gem 'logstash-logger', require: false
gem "logster" gem 'logster'
# These are forks of sassc and sassc-rails with dart-sass support # NOTE: later versions of sassc are causing a segfault, possibly dependent on processer architecture
gem "dartsass-ruby" # and until resolved should be locked at 2.0.1
gem "dartsass-sprockets" gem 'sassc', '2.0.1', require: false
gem "sassc-rails"
gem "rotp", require: false gem 'rotp', require: false
gem "rqrcode" gem 'rqrcode'
gem "rubyzip", require: false gem 'rubyzip', require: false
gem "sshkey", require: false gem 'sshkey', require: false
gem "rchardet", require: false gem 'rchardet', require: false
gem "lz4-ruby", require: false, platform: :ruby gem 'lz4-ruby', require: false, platform: :ruby
gem "sanitize" gem 'sanitize'
if ENV["IMPORT"] == "1" if ENV["IMPORT"] == "1"
gem "mysql2" gem 'mysql2'
gem "redcarpet" gem 'redcarpet'
# NOTE: in import mode the version of sqlite can matter a lot, so we stick it to a specific one # NOTE: in import mode the version of sqlite can matter a lot, so we stick it to a specific one
gem "sqlite3", "~> 1.3", ">= 1.3.13" gem 'sqlite3', '~> 1.3', '>= 1.3.13'
gem "ruby-bbcode-to-md", git: "https://github.com/nlalonde/ruby-bbcode-to-md" gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md'
gem "reverse_markdown" gem 'reverse_markdown'
gem "tiny_tds" gem 'tiny_tds'
gem "csv" gem 'csv'
gem "parallel", require: false gem 'parallel', require: false
end end
gem "web-push" # workaround for openssl 3.0, see
gem "colored2", require: false # https://github.com/pushpad/web-push/pull/2
gem "maxminddb" gem 'web-push', require: false, git: 'https://github.com/xfalcox/web-push', branch: 'openssl-3-compat'
gem 'colored2', require: false
gem 'maxminddb'
gem "rails_failover", require: false gem 'rails_failover', require: false
gem "faraday" gem 'faraday'
gem "faraday-retry" gem 'faraday-retry'
# workaround for faraday-net_http, see # workaround for faraday-net_http, see
# https://github.com/ruby/net-imap/issues/16#issuecomment-803086765 # https://github.com/ruby/net-imap/issues/16#issuecomment-803086765
gem "net-http" gem 'net-http'
# workaround for prometheus-client # workaround for prometheus-client
gem "webrick", require: false gem 'webrick', require: false
# Workaround until Ruby ships with cgi version 0.3.6 or higher. # Workaround until Ruby ships with cgi version 0.3.6 or higher.
gem "cgi", ">= 0.3.6", require: false gem "cgi", ">= 0.3.6", require: false
gem "tzinfo-data"

View File

@ -6,36 +6,37 @@ GIT
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
GIT GIT
remote: https://github.com/rails/sprockets remote: https://github.com/xfalcox/web-push
revision: f4d3dae71ef29c44b75a49cfbf8032cce07b423a revision: 369df8f475a4cd4832a7679bec16576665f24d24
branch: 3.x branch: openssl-3-compat
specs: specs:
sprockets (3.7.2) web-push (2.1.0)
concurrent-ruby (~> 1.0) hkdf (~> 1.0)
rack (> 1, < 3) jwt (~> 2.0)
openssl (~> 3.0)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actionmailer (7.0.4.3) actionmailer (7.0.3.1)
actionpack (= 7.0.4.3) actionpack (= 7.0.3.1)
actionview (= 7.0.4.3) actionview (= 7.0.3.1)
activejob (= 7.0.4.3) activejob (= 7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (7.0.4.3) actionpack (7.0.3.1)
actionview (= 7.0.4.3) actionview (= 7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
rack (~> 2.0, >= 2.2.0) rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (7.0.4.3) actionview (7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -44,15 +45,15 @@ GEM
actionview (>= 6.0.a) actionview (>= 6.0.a)
active_model_serializers (0.8.4) active_model_serializers (0.8.4)
activemodel (>= 3.0) activemodel (>= 3.0)
activejob (7.0.4.3) activejob (7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.4.3) activemodel (7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
activerecord (7.0.4.3) activerecord (7.0.3.1)
activemodel (= 7.0.4.3) activemodel (= 7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
activesupport (7.0.4.3) activesupport (7.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -88,10 +89,10 @@ GEM
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (1.0.0) binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.16.0) bootsnap (1.15.0)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.2.4) builder (3.2.4)
bullet (7.0.7) bullet (7.0.5)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
byebug (11.1.3) byebug (11.1.3)
@ -110,7 +111,7 @@ GEM
chunky_png (1.4.0) chunky_png (1.4.0)
coderay (1.1.3) coderay (1.1.3)
colored2 (3.1.2) colored2 (3.1.2)
concurrent-ruby (1.2.2) concurrent-ruby (1.1.10)
connection_pool (2.3.0) connection_pool (2.3.0)
cose (1.3.0) cose (1.3.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
@ -119,17 +120,8 @@ GEM
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.14.0) css_parser (1.13.0)
addressable addressable
dartsass-ruby (3.0.1)
sass-embedded (~> 1.54)
dartsass-sprockets (3.0.0)
dartsass-ruby (~> 3.0)
railties (>= 4.0.0)
sprockets (> 3.0)
sprockets-rails
tilt
date (3.3.3)
debug_inspector (1.1.0) debug_inspector (1.1.0)
diff-lcs (1.5.0) diff-lcs (1.5.0)
diffy (3.4.2) diffy (3.4.2)
@ -145,19 +137,19 @@ GEM
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
email_reply_trimmer (0.1.13) email_reply_trimmer (0.1.13)
erubi (1.12.0) erubi (1.11.0)
excon (0.99.0) excon (0.95.0)
execjs (2.8.1) execjs (2.8.1)
exifr (1.3.10) exifr (1.3.10)
fabrication (2.30.0) fabrication (2.30.0)
faker (2.23.0) faker (2.23.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
fakeweb (1.3.0) fakeweb (1.3.0)
faraday (2.7.4) faraday (2.7.2)
faraday-net_http (>= 2.0, < 3.1) faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4) ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.2) faraday-net_http (3.0.2)
faraday-retry (2.1.0) faraday-retry (2.0.0)
faraday (~> 2.0) faraday (~> 2.0)
fast_blank (1.0.1) fast_blank (1.0.1)
fast_xs (0.8.0) fast_xs (0.8.0)
@ -165,13 +157,8 @@ GEM
ffi (1.15.5) ffi (1.15.5)
fspath (3.1.2) fspath (3.1.2)
gc_tracer (1.5.1) gc_tracer (1.5.1)
globalid (1.1.0) globalid (1.0.0)
activesupport (>= 5.0) activesupport (>= 5.0)
google-protobuf (3.22.2)
google-protobuf (3.22.2-aarch64-linux)
google-protobuf (3.22.2-arm64-darwin)
google-protobuf (3.22.2-x86_64-darwin)
google-protobuf (3.22.2-x86_64-linux)
guess_html_encoding (0.0.11) guess_html_encoding (0.0.11)
hana (1.3.7) hana (1.3.7)
hashdiff (1.0.1) hashdiff (1.0.1)
@ -182,7 +169,7 @@ GEM
http_accept_language (2.1.1) http_accept_language (2.1.1)
i18n (1.12.0) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_optim (0.31.3) image_optim (0.31.2)
exifr (~> 1.2, >= 1.2.2) exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0) fspath (~> 3.0)
image_size (>= 1.5, < 4) image_size (>= 1.5, < 4)
@ -199,7 +186,7 @@ GEM
hana (~> 1.3) hana (~> 1.3)
regexp_parser (~> 2.0) regexp_parser (~> 2.0)
uri_template (~> 0.7) uri_template (~> 0.7)
jwt (2.7.0) jwt (2.6.0)
kgio (2.11.4) kgio (2.11.4)
libv8-node (16.10.0.0) libv8-node (16.10.0.0)
libv8-node (16.10.0.0-aarch64-linux) libv8-node (16.10.0.0-aarch64-linux)
@ -207,7 +194,7 @@ GEM
libv8-node (16.10.0.0-x86_64-darwin) libv8-node (16.10.0.0-x86_64-darwin)
libv8-node (16.10.0.0-x86_64-darwin-19) libv8-node (16.10.0.0-x86_64-darwin-19)
libv8-node (16.10.0.0-x86_64-linux) libv8-node (16.10.0.0-x86_64-linux)
listen (3.8.0) listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
literate_randomizer (0.4.0) literate_randomizer (0.4.0)
@ -219,7 +206,7 @@ GEM
logstash-event (1.2.02) logstash-event (1.2.02)
logstash-logger (0.26.1) logstash-logger (0.26.1)
logstash-event (~> 1.2) logstash-event (~> 1.2)
logster (2.12.2) logster (2.11.3)
loofah (2.19.1) loofah (2.19.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
@ -228,7 +215,7 @@ GEM
matrix (0.4.2) matrix (0.4.2)
maxminddb (0.1.22) maxminddb (0.1.22)
memory_profiler (1.0.1) memory_profiler (1.0.1)
message_bus (4.3.2) message_bus (4.3.0)
rack (>= 1.1.3) rack (>= 1.1.3)
method_source (1.0.0) method_source (1.0.0)
mini_mime (1.1.2) mini_mime (1.1.2)
@ -240,17 +227,16 @@ GEM
mini_sql (1.4.0) mini_sql (1.4.0)
mini_suffix (0.3.3) mini_suffix (0.3.3)
ffi (~> 1.9) ffi (~> 1.9)
minitest (5.18.0) minitest (5.17.0)
mocha (2.0.2) mocha (2.0.2)
ruby2_keywords (>= 0.0.5) ruby2_keywords (>= 0.0.5)
msgpack (1.6.1) msgpack (1.6.0)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
mustache (1.1.1) mustache (1.1.1)
net-http (0.3.2) net-http (0.3.2)
uri uri
net-imap (0.3.4) net-imap (0.3.1)
date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
@ -259,16 +245,16 @@ GEM
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.14.2) nokogiri (1.13.10)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.14.2-aarch64-linux) nokogiri (1.13.10-aarch64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.14.2-arm64-darwin) nokogiri (1.13.10-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.14.2-x86_64-darwin) nokogiri (1.13.10-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.14.2-x86_64-linux) nokogiri (1.13.10-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (1.1.0) oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1) oauth-tty (~> 1.0, >= 1.0.1)
@ -305,19 +291,18 @@ GEM
omniauth-twitter (1.4.0) omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
rack rack
openssl (3.1.0) openssl (3.0.2)
openssl-signature_algorithm (1.3.0) openssl-signature_algorithm (1.2.1)
openssl (> 2.0) openssl (> 2.0, < 3.1)
optimist (3.0.1) optimist (3.0.1)
parallel (1.22.1) parallel (1.22.1)
parallel_tests (4.2.0) parallel_tests (4.0.0)
parallel parallel
parser (3.2.1.1) parser (3.1.3.0)
ast (~> 2.4.1) ast (~> 2.4.1)
pg (1.4.6) pg (1.4.5)
prettier_print (1.2.1)
progress (3.6.0) progress (3.6.0)
pry (0.14.2) pry (0.14.1)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-byebug (3.10.1) pry-byebug (3.10.1)
@ -326,20 +311,21 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (5.0.1) public_suffix (5.0.1)
puma (6.1.1) puma (6.0.1)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.6.2) r2 (0.2.7)
rack (2.2.6.4) racc (1.6.1)
rack (2.2.5)
rack-mini-profiler (3.0.0) rack-mini-profiler (3.0.0)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-protection (3.0.5) rack-protection (3.0.5)
rack rack
rack-test (2.1.0) rack-test (2.0.2)
rack (>= 1.3) rack (>= 1.3)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0) rails-html-sanitizer (1.4.4)
loofah (~> 2.19, >= 2.19.1) loofah (~> 2.19, >= 2.19.1)
rails_failover (0.8.1) rails_failover (0.8.1)
activerecord (> 6.0, < 7.1) activerecord (> 6.0, < 7.1)
@ -348,15 +334,15 @@ GEM
rails_multisite (4.0.1) rails_multisite (4.0.1)
activerecord (> 5.0, < 7.1) activerecord (> 5.0, < 7.1)
railties (> 5.0, < 7.1) railties (> 5.0, < 7.1)
railties (7.0.4.3) railties (7.0.3.1)
actionpack (= 7.0.4.3) actionpack (= 7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
raindrops (0.20.1) raindrops (0.20.0)
rake (13.0.6) rake (13.0.6)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
@ -366,10 +352,10 @@ GEM
msgpack (>= 0.4.3) msgpack (>= 0.4.3)
optimist (>= 3.0.0) optimist (>= 3.0.0)
rchardet (1.8.0) rchardet (1.8.0)
redis (4.8.1) redis (4.8.0)
redis-namespace (1.10.0) redis-namespace (1.9.0)
redis (>= 4) redis (>= 4)
regexp_parser (2.7.0) regexp_parser (2.6.1)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
rexml (3.2.5) rexml (3.2.5)
@ -383,15 +369,15 @@ GEM
rspec-core (~> 3.12.0) rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0) rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0) rspec-mocks (~> 3.12.0)
rspec-core (3.12.1) rspec-core (3.12.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-expectations (3.12.2) rspec-expectations (3.12.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-html-matchers (0.10.0) rspec-html-matchers (0.10.0)
nokogiri (~> 1) nokogiri (~> 1)
rspec (>= 3.0.0.a) rspec (>= 3.0.0.a)
rspec-mocks (3.12.4) rspec-mocks (3.12.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.0.1) rspec-rails (6.0.1)
@ -410,50 +396,43 @@ GEM
json-schema (>= 2.2, < 4.0) json-schema (>= 2.2, < 4.0)
railties (>= 3.1, < 7.1) railties (>= 3.1, < 7.1)
rspec-core (>= 2.14) rspec-core (>= 2.14)
rtlcss (0.2.0) rubocop (1.42.0)
mini_racer (~> 0.6.3)
rubocop (1.48.1)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.0.0) parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.26.0, < 2.0) rubocop-ast (>= 1.24.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.27.0) rubocop-ast (1.24.1)
parser (>= 3.2.1.0) parser (>= 3.1.1.0)
rubocop-capybara (2.17.1) rubocop-discourse (3.0.1)
rubocop (~> 1.41)
rubocop-discourse (3.2.0)
rubocop (>= 1.1.0) rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0) rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.19.0) rubocop-rspec (2.16.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara (~> 2.17) ruby-prof (1.4.5)
ruby-prof (1.6.1) ruby-progressbar (1.11.0)
ruby-progressbar (1.13.0)
ruby-readability (0.7.0) ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4) guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0) nokogiri (>= 1.6.0)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
sanitize (6.0.1) sanitize (6.0.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
sass-embedded (1.59.2) sassc (2.0.1)
google-protobuf (~> 3.21) ffi (~> 1.9)
rake (>= 10.0.0) rake
sass-embedded (1.59.2-aarch64-linux-gnu) sassc-rails (2.1.2)
google-protobuf (~> 3.21) railties (>= 4.0.0)
sass-embedded (1.59.2-arm64-darwin) sassc (>= 2.0)
google-protobuf (~> 3.21) sprockets (> 3.0)
sass-embedded (1.59.2-x86_64-darwin) sprockets-rails
google-protobuf (~> 3.21) tilt
sass-embedded (1.59.2-x86_64-linux-gnu) selenium-webdriver (4.7.1)
google-protobuf (~> 3.21)
selenium-webdriver (4.8.1)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
@ -463,7 +442,7 @@ GEM
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.5, < 3)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.5.0, < 5) redis (>= 4.5.0, < 5)
simplecov (0.22.0) simplecov (0.21.2)
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
@ -472,29 +451,27 @@ GEM
snaky_hash (2.0.1) snaky_hash (2.0.1)
hashie hashie
version_gem (~> 1.1, >= 1.1.1) version_gem (~> 1.1, >= 1.1.1)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkey (2.0.0) sshkey (2.0.0)
stackprof (0.2.23) stackprof (0.2.23)
syntax_tree (6.0.2) test-prof (1.1.0)
prettier_print (>= 1.2.0)
syntax_tree-disable_ternary (1.0.0)
test-prof (1.2.0)
thor (1.2.1) thor (1.2.1)
tilt (2.1.0) tilt (2.0.11)
timeout (0.3.2) timeout (0.3.1)
tzinfo (2.0.6) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2022.7)
tzinfo (>= 1.0.0)
uglifier (4.2.0) uglifier (4.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (2.4.2) unicode-display_width (2.3.0)
unicorn (6.1.0) unicorn (6.1.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
@ -502,10 +479,6 @@ GEM
uri (0.12.0) uri (0.12.0)
uri_template (0.7.0) uri_template (0.7.0)
version_gem (1.1.1) version_gem (1.1.1)
web-push (3.0.0)
hkdf (~> 1.0)
jwt (~> 2.0)
openssl (~> 3.0)
webdrivers (5.2.0) webdrivers (5.2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
@ -519,10 +492,8 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yaml-lint (0.1.2) yaml-lint (0.0.10)
yard (0.9.28) zeitwerk (2.6.6)
webrick (~> 1.7.0)
zeitwerk (2.6.7)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@ -534,14 +505,14 @@ PLATFORMS
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
actionmailer (= 7.0.4.3) actionmailer (= 7.0.3.1)
actionpack (= 7.0.4.3) actionpack (= 7.0.3.1)
actionview (= 7.0.4.3) actionview (= 7.0.3.1)
actionview_precompiler actionview_precompiler
active_model_serializers (~> 0.8.3) active_model_serializers (~> 0.8.3)
activemodel (= 7.0.4.3) activemodel (= 7.0.3.1)
activerecord (= 7.0.4.3) activerecord (= 7.0.3.1)
activesupport (= 7.0.4.3) activesupport (= 7.0.3.1)
addressable addressable
annotate annotate
aws-sdk-s3 aws-sdk-s3
@ -559,8 +530,6 @@ DEPENDENCIES
cose cose
cppjieba_rb cppjieba_rb
css_parser css_parser
dartsass-ruby
dartsass-sprockets
diffy diffy
digest digest
discourse-fonts discourse-fonts
@ -622,12 +591,13 @@ DEPENDENCIES
pry-byebug pry-byebug
pry-rails pry-rails
puma puma
r2
rack rack
rack-mini-profiler rack-mini-profiler
rack-protection rack-protection
rails_failover rails_failover
rails_multisite rails_multisite
railties (= 7.0.4.3) railties (= 7.0.3.1)
rake rake
rb-fsevent rb-fsevent
rbtrace rbtrace
@ -642,35 +612,32 @@ DEPENDENCIES
rspec-rails rspec-rails
rss rss
rswag-specs rswag-specs
rtlcss
rubocop-discourse rubocop-discourse
ruby-prof ruby-prof
ruby-readability ruby-readability
rubyzip rubyzip
sanitize sanitize
sassc (= 2.0.1)
sassc-rails
selenium-webdriver selenium-webdriver
shoulda-matchers shoulda-matchers
sidekiq sidekiq
simplecov simplecov
sprockets! sprockets (= 3.7.2)
sprockets-rails sprockets-rails
sshkey sshkey
stackprof stackprof
syntax_tree
syntax_tree-disable_ternary
test-prof test-prof
thor thor
tzinfo-data
uglifier uglifier
unf unf
unicorn unicorn
web-push web-push!
webdrivers webdrivers
webmock webmock
webrick webrick
xorcist xorcist
yaml-lint yaml-lint
yard
BUNDLED WITH BUNDLED WITH
2.4.4 2.3.22

View File

@ -30,7 +30,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. 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 3.2+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 13](https://www.postgresql.org/download/), [Redis 7](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.7+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 13+](https://www.postgresql.org/download/), [Redis 6.2+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
## Setting up Discourse ## Setting up Discourse
@ -51,7 +51,7 @@ Discourse supports the **latest, stable releases** of all major browsers and pla
| Microsoft Edge | | | | Microsoft Edge | | |
| Mozilla Firefox | | | | Mozilla Firefox | | |
Additionally, we aim to support Safari on iOS 15.7+. Additionally, we aim to support Safari on iOS 12.5+ until January 2023 (Discourse 3.0).
## Built With ## Built With
@ -91,7 +91,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
## Copyright / License ## Copyright / License
Copyright 2014 - 2023 Civilized Discourse Construction Kit, Inc. Copyright 2014 - 2022 Civilized Discourse Construction Kit, Inc.
Licensed under the GNU General Public License Version 2.0 (or later); Licensed under the GNU General Public License Version 2.0 (or later);
you may not use this work except in compliance with the License. you may not use this work except in compliance with the License.

View File

@ -1,13 +1,13 @@
import RestAdapter from "discourse/adapters/rest"; import RESTAdapter from "discourse/adapters/rest";
export default class ApiKey extends RestAdapter { export default RESTAdapter.extend({
jsonMode = true; jsonMode: true,
basePath() { basePath() {
return "/admin/api/"; return "/admin/api/";
} },
apiNameFor() { apiNameFor() {
return "key"; return "key";
} },
} });

View File

@ -1,11 +1,11 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default function buildPluginAdapter(pluginName) { export default function buildPluginAdapter(pluginName) {
return class extends RestAdapter { return RestAdapter.extend({
pathFor(store, type, findArgs) { pathFor(store, type, findArgs) {
return ( return (
"/admin/plugins/" + pluginName + super.pathFor(store, type, findArgs) "/admin/plugins/" + pluginName + this._super(store, type, findArgs)
); );
} },
}; });
} }

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class CustomizationBase extends RestAdapter { export default RestAdapter.extend({
basePath() { basePath() {
return "/admin/customize/"; return "/admin/customize/";
} },
} });

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class EmailStyle extends RestAdapter { export default RestAdapter.extend({
pathFor() { pathFor() {
return "/admin/customize/email_style"; return "/admin/customize/email_style";
} },
} });

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class Embedding extends RestAdapter { export default RestAdapter.extend({
pathFor() { pathFor() {
return "/admin/customize/embedding"; return "/admin/customize/embedding";
} },
} });

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class StaffActionLog extends RestAdapter { export default RestAdapter.extend({
basePath() { basePath() {
return "/admin/logs/"; return "/admin/logs/";
} },
} });

View File

@ -1,5 +1,5 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class TagGroup extends RestAdapter { export default RestAdapter.extend({
jsonMode = true; jsonMode: true,
} });

View File

@ -1,10 +1,9 @@
import RestAdapter from "discourse/adapters/rest"; import RestAdapter from "discourse/adapters/rest";
export default class Theme extends RestAdapter { export default RestAdapter.extend({
jsonMode = true;
basePath() { basePath() {
return "/admin/"; return "/admin/";
} },
afterFindAll(results) { afterFindAll(results) {
let map = {}; let map = {};
@ -21,5 +20,7 @@ export default class Theme extends RestAdapter {
theme.set("parentThemes", mappedParents); theme.set("parentThemes", mappedParents);
}); });
return results; return results;
} },
}
jsonMode: true,
});

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest"; import RESTAdapter from "discourse/adapters/rest";
export default class WebHookEvent extends RestAdapter { export default RESTAdapter.extend({
basePath() { basePath() {
return "/admin/api/"; return "/admin/api/";
} },
} });

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest"; import RESTAdapter from "discourse/adapters/rest";
export default class WebHook extends RestAdapter { export default RESTAdapter.extend({
basePath() { basePath() {
return "/admin/api/"; return "/admin/api/";
} },
} });

View File

@ -1,33 +1,31 @@
import { action } from "@ember/object";
import { classNames } from "@ember-decorators/component";
import { observes, on } from "@ember-decorators/object";
import Component from "@ember/component"; import Component from "@ember/component";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import loadScript from "discourse/lib/load-script"; import loadScript from "discourse/lib/load-script";
import I18n from "I18n"; import I18n from "I18n";
import { bind } from "discourse-common/utils/decorators"; import { bind, observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
const COLOR_VARS_REGEX = const COLOR_VARS_REGEX =
/\$(primary|secondary|tertiary|quaternary|header_background|header_primary|highlight|danger|success|love)(\s|;|-(low|medium|high))/g; /\$(primary|secondary|tertiary|quaternary|header_background|header_primary|highlight|danger|success|love)(\s|;|-(low|medium|high))/g;
@classNames("ace-wrapper") export default Component.extend({
export default class AceEditor extends Component { mode: "css",
mode = "css"; classNames: ["ace-wrapper"],
disabled = false; _editor: null,
htmlPlaceholder = false; _skipContentChangeEvent: null,
_editor = null; disabled: false,
_skipContentChangeEvent = null; htmlPlaceholder: false,
@observes("editorId") @observes("editorId")
editorIdChanged() { editorIdChanged() {
if (this.autofocus) { if (this.autofocus) {
this.send("focus"); this.send("focus");
} }
} },
didRender() { didRender() {
this._skipContentChangeEvent = false; this._skipContentChangeEvent = false;
} },
@observes("content") @observes("content")
contentChanged() { contentChanged() {
@ -35,14 +33,14 @@ export default class AceEditor extends Component {
if (this._editor && !this._skipContentChangeEvent) { if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content); this._editor.getSession().setValue(content);
} }
} },
@observes("mode") @observes("mode")
modeChanged() { modeChanged() {
if (this._editor && !this._skipContentChangeEvent) { if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode); this._editor.getSession().setMode("ace/mode/" + this.mode);
} }
} },
@observes("placeholder") @observes("placeholder")
placeholderChanged() { placeholderChanged() {
@ -51,12 +49,12 @@ export default class AceEditor extends Component {
placeholder: this.placeholder, placeholder: this.placeholder,
}); });
} }
} },
@observes("disabled") @observes("disabled")
disabledStateChanged() { disabledStateChanged() {
this.changeDisabledState(); this.changeDisabledState();
} },
changeDisabledState() { changeDisabledState() {
const editor = this._editor; const editor = this._editor;
@ -69,10 +67,9 @@ export default class AceEditor extends Component {
}); });
editor.container.parentNode.setAttribute("data-disabled", disabled); editor.container.parentNode.setAttribute("data-disabled", disabled);
} }
} },
@on("willDestroyElement") _destroyEditor: on("willDestroyElement", function () {
_destroyEditor() {
if (this._editor) { if (this._editor) {
this._editor.destroy(); this._editor.destroy();
this._editor = null; this._editor = null;
@ -83,16 +80,16 @@ export default class AceEditor extends Component {
} }
$(window).off("ace:resize"); $(window).off("ace:resize");
} }),
resize() { resize() {
if (this._editor) { if (this._editor) {
this._editor.resize(); this._editor.resize();
} }
} },
didInsertElement() { didInsertElement() {
super.didInsertElement(...arguments); this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => { loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], (loadedAce) => { window.ace.require(["ace/ace"], (loadedAce) => {
loadedAce.config.set("loadWorkerFromBlob", false); loadedAce.config.set("loadWorkerFromBlob", false);
@ -156,13 +153,13 @@ export default class AceEditor extends Component {
this._darkModeListener.addListener(this.setAceTheme); this._darkModeListener.addListener(this.setAceTheme);
}); });
}); });
} },
willDestroyElement() { willDestroyElement() {
if (this._darkModeListener) { if (this._darkModeListener) {
this._darkModeListener.removeListener(this.setAceTheme); this._darkModeListener.removeListener(this.setAceTheme);
} }
} },
@bind @bind
setAceTheme() { setAceTheme() {
@ -173,7 +170,7 @@ export default class AceEditor extends Component {
this._editor.setTheme( this._editor.setTheme(
`ace/theme/${schemeType === "dark" ? "chaos" : "chrome"}` `ace/theme/${schemeType === "dark" ? "chaos" : "chrome"}`
); );
} },
warnSCSSDeprecations() { warnSCSSDeprecations() {
if ( if (
@ -200,20 +197,21 @@ export default class AceEditor extends Component {
this._editor.getSession().setAnnotations(warnings); this._editor.getSession().setAnnotations(warnings);
this.setWarning?.( this.setWarning(
warnings.length warnings.length
? I18n.t("admin.customize.theme.scss_color_variables_warning") ? I18n.t("admin.customize.theme.scss_color_variables_warning")
: false : false
); );
} },
@action actions: {
focus() { focus() {
if (this._editor) { if (this._editor) {
this._editor.focus(); this._editor.focus();
this._editor.navigateFileEnd(); this._editor.navigateFileEnd();
} }
} },
},
_overridePlaceholder(loadedAce) { _overridePlaceholder(loadedAce) {
const originalPlaceholderSetter = const originalPlaceholderSetter =
@ -241,5 +239,5 @@ export default class AceEditor extends Component {
this.$updatePlaceholder(); this.$updatePlaceholder();
}; };
} },
} });

View File

@ -1,26 +1,28 @@
import { classNames } from "@ember-decorators/component"; import { observes, on } from "discourse-common/utils/decorators";
import { observes, on } from "@ember-decorators/object";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { scheduleOnce } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop";
@classNames("admin-backups-logs") export default Component.extend({
export default class AdminBackupsLogs extends Component { classNames: ["admin-backups-logs"],
showLoadingSpinner = false; showLoadingSpinner: false,
hasFormattedLogs = false; hasFormattedLogs: false,
noLogsMessage = I18n.t("admin.backups.logs.none"); noLogsMessage: I18n.t("admin.backups.logs.none"),
formattedLogs = "";
index = 0; init() {
this._super(...arguments);
this._reset();
},
_reset() { _reset() {
this.setProperties({ formattedLogs: "", index: 0 }); this.setProperties({ formattedLogs: "", index: 0 });
} },
_scrollDown() { _scrollDown() {
const div = this.element; const div = this.element;
div.scrollTop = div.scrollHeight; div.scrollTop = div.scrollHeight;
} },
@on("init") @on("init")
@observes("logs.[]") @observes("logs.[]")
@ -29,7 +31,7 @@ export default class AdminBackupsLogs extends Component {
this._reset(); // reset the cached logs whenever the model is reset this._reset(); // reset the cached logs whenever the model is reset
this.renderLogs(); this.renderLogs();
} }
} },
_updateFormattedLogsFunc() { _updateFormattedLogsFunc() {
const logs = this.logs; const logs = this.logs;
@ -53,13 +55,13 @@ export default class AdminBackupsLogs extends Component {
this.renderLogs(); this.renderLogs();
scheduleOnce("afterRender", this, this._scrollDown); scheduleOnce("afterRender", this, this._scrollDown);
} },
@on("init") @on("init")
@observes("logs.[]") @observes("logs.[]")
_updateFormattedLogs() { _updateFormattedLogs() {
discourseDebounce(this, this._updateFormattedLogsFunc, 150); discourseDebounce(this, this._updateFormattedLogsFunc, 150);
} },
renderLogs() { renderLogs() {
const formattedLogs = this.formattedLogs; const formattedLogs = this.formattedLogs;
@ -74,5 +76,5 @@ export default class AdminBackupsLogs extends Component {
} else { } else {
this.set("showLoadingSpinner", false); this.set("showLoadingSpinner", false);
} }
} },
} });

View File

@ -1,22 +1,28 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
@tagName("") export default Component.extend({
export default class AdminEditableField extends Component { tagName: "",
buffer = "";
editing = false; buffer: "",
editing: false,
init() {
this._super(...arguments);
this.set("editing", false);
},
@action @action
edit(event) { edit(event) {
event?.preventDefault(); event?.preventDefault();
this.set("buffer", this.value); this.set("buffer", this.value);
this.toggleProperty("editing"); this.toggleProperty("editing");
} },
@action actions: {
save() { save() {
// Action has to toggle 'editing' property. // Action has to toggle 'editing' property.
this.action(this.buffer); this.action(this.buffer);
} },
} },
});

View File

@ -1,5 +1,4 @@
import { classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@classNames("row") classNames: ["row"],
export default class AdminFormRow extends Component {} });

View File

@ -1,10 +1,9 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import loadScript from "discourse/lib/load-script"; import loadScript from "discourse/lib/load-script";
@tagName("canvas") export default Component.extend({
export default class AdminGraph extends Component { tagName: "canvas",
type = "line"; type: "line",
refreshChart() { refreshChart() {
const ctx = this.element.getContext("2d"); const ctx = this.element.getContext("2d");
@ -50,11 +49,11 @@ export default class AdminGraph extends Component {
}; };
this._chart = new window.Chart(ctx, config); this._chart = new window.Chart(ctx, config);
} },
didInsertElement() { didInsertElement() {
loadScript("/javascripts/Chart.min.js").then(() => loadScript("/javascripts/Chart.min.js").then(() =>
this.refreshChart.apply(this) this.refreshChart.apply(this)
); );
} },
} });

View File

@ -1,5 +1,4 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("") tagName: "",
export default class AdminNav extends Component {} });

View File

@ -1,16 +1,16 @@
import { classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@classNames("penalty-history") export default Component.extend({
export default class AdminPenaltyHistory extends Component { classNames: ["penalty-history"],
@discourseComputed("user.penalty_counts.suspended") @discourseComputed("user.penalty_counts.suspended")
suspendedCountClass(count) { suspendedCountClass(count) {
if (count > 0) { if (count > 0) {
return "danger"; return "danger";
} }
return ""; return "";
} },
@discourseComputed("user.penalty_counts.silenced") @discourseComputed("user.penalty_counts.silenced")
silencedCountClass(count) { silencedCountClass(count) {
@ -18,5 +18,5 @@ export default class AdminPenaltyHistory extends Component {
return "danger"; return "danger";
} }
return ""; return "";
} },
} });

View File

@ -1,6 +1,5 @@
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { equal } from "@ember/object/computed";
import discourseComputed, { import discourseComputed, {
afterRender, afterRender,
} from "discourse-common/utils/decorators"; } from "discourse-common/utils/decorators";
@ -8,28 +7,30 @@ import I18n from "I18n";
const ACTIONS = ["delete", "delete_replies", "edit", "none"]; const ACTIONS = ["delete", "delete_replies", "edit", "none"];
export default class AdminPenaltyPostAction extends Component { export default Component.extend({
postId = null; postId: null,
postAction = null; postAction: null,
postEdit = null; postEdit: null,
@equal("postAction", "edit") editing;
@discourseComputed @discourseComputed
penaltyActions() { penaltyActions() {
return ACTIONS.map((id) => { return ACTIONS.map((id) => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) }; return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
}); });
} },
@action editing: equal("postAction", "edit"),
penaltyChanged(postAction) {
this.set("postAction", postAction);
// If we switch to edit mode, jump to the edit textarea actions: {
if (postAction === "edit") { penaltyChanged(postAction) {
this._focusEditTextarea(); this.set("postAction", postAction);
}
} // If we switch to edit mode, jump to the edit textarea
if (postAction === "edit") {
this._focusEditTextarea();
}
},
},
@afterRender @afterRender
_focusEditTextarea() { _focusEditTextarea() {
@ -37,5 +38,5 @@ export default class AdminPenaltyPostAction extends Component {
const body = elem.closest(".modal-body"); const body = elem.closest(".modal-body");
body.scrollTo(0, body.clientHeight); body.scrollTo(0, body.clientHeight);
elem.querySelector(".post-editor").focus(); elem.querySelector(".post-editor").focus();
} },
} });

View File

@ -1,46 +1,43 @@
import { tagName } from "@ember-decorators/component";
import { equal } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n"; import I18n from "I18n";
const CUSTOM_REASON_KEY = "custom"; const CUSTOM_REASON_KEY = "custom";
@tagName("") export default Component.extend({
export default class AdminPenaltyReason extends Component { tagName: "",
selectedReason = CUSTOM_REASON_KEY; selectedReason: CUSTOM_REASON_KEY,
customReason = ""; customReason: "",
reasonKeys: [
reasonKeys = [
"not_listening_to_staff", "not_listening_to_staff",
"consuming_staff_time", "consuming_staff_time",
"combative", "combative",
"in_wrong_place", "in_wrong_place",
"no_constructive_purpose", "no_constructive_purpose",
CUSTOM_REASON_KEY, CUSTOM_REASON_KEY,
]; ],
isCustomReason: equal("selectedReason", CUSTOM_REASON_KEY),
@equal("selectedReason", CUSTOM_REASON_KEY) isCustomReason;
@discourseComputed("reasonKeys") @discourseComputed("reasonKeys")
reasons(keys) { reasons(keys) {
return keys.map((key) => { return keys.map((key) => {
return { id: key, name: I18n.t(`admin.user.suspend_reasons.${key}`) }; return { id: key, name: I18n.t(`admin.user.suspend_reasons.${key}`) };
}); });
} },
@action @action
setSelectedReason(value) { setSelectedReason(value) {
this.set("selectedReason", value); this.set("selectedReason", value);
this.setReason(); this.setReason();
} },
@action @action
setCustomReason(value) { setCustomReason(value) {
this.set("customReason", value); this.set("customReason", value);
this.setReason(); this.setReason();
} },
setReason() { setReason() {
if (this.isCustomReason) { if (this.isCustomReason) {
@ -51,5 +48,5 @@ export default class AdminPenaltyReason extends Component {
I18n.t(`admin.user.suspend_reasons.${this.selectedReason}`) I18n.t(`admin.user.suspend_reasons.${this.selectedReason}`)
); );
} }
} },
} });

View File

@ -1,10 +1,10 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@tagName("") export default Component.extend({
export default class AdminPenaltySimilarUsers extends Component { tagName: "",
@discourseComputed("penaltyType") @discourseComputed("penaltyType")
penaltyField(penaltyType) { penaltyField(penaltyType) {
if (penaltyType === "suspend") { if (penaltyType === "suspend") {
@ -12,7 +12,7 @@ export default class AdminPenaltySimilarUsers extends Component {
} else if (penaltyType === "silence") { } else if (penaltyType === "silence") {
return "can_be_silenced"; return "can_be_silenced";
} }
} },
@action @action
selectUserId(userId, event) { selectUserId(userId, event) {
@ -25,5 +25,5 @@ export default class AdminPenaltySimilarUsers extends Component {
} else { } else {
this.selectedUserIds.removeObject(userId); this.selectedUserIds.removeObject(userId);
} }
} },
} });

View File

@ -1,4 +1,3 @@
import { classNames } from "@ember-decorators/component";
import Report from "admin/models/report"; import Report from "admin/models/report";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
@ -8,31 +7,31 @@ import { number } from "discourse/lib/formatter";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
@classNames("admin-report-chart") export default Component.extend({
export default class AdminReportChart extends Component { classNames: ["admin-report-chart"],
limit = 8; limit: 8,
total = 0; total: 0,
options = null; options: null,
didInsertElement() { didInsertElement() {
super.didInsertElement(...arguments); this._super(...arguments);
window.addEventListener("resize", this._resizeHandler); window.addEventListener("resize", this._resizeHandler);
} },
willDestroyElement() { willDestroyElement() {
super.willDestroyElement(...arguments); this._super(...arguments);
window.removeEventListener("resize", this._resizeHandler); window.removeEventListener("resize", this._resizeHandler);
this._resetChart(); this._resetChart();
} },
didReceiveAttrs() { didReceiveAttrs() {
super.didReceiveAttrs(...arguments); this._super(...arguments);
discourseDebounce(this, this._scheduleChartRendering, 100); discourseDebounce(this, this._scheduleChartRendering, 100);
} },
_scheduleChartRendering() { _scheduleChartRendering() {
schedule("afterRender", () => { schedule("afterRender", () => {
@ -41,7 +40,7 @@ export default class AdminReportChart extends Component {
this.element && this.element.querySelector(".chart-canvas") this.element && this.element.querySelector(".chart-canvas")
); );
}); });
} },
_renderChart(model, chartCanvas) { _renderChart(model, chartCanvas) {
if (!chartCanvas) { if (!chartCanvas) {
@ -100,7 +99,7 @@ export default class AdminReportChart extends Component {
this._buildChartConfig(data, this.options) this._buildChartConfig(data, this.options)
); );
}); });
} },
_buildChartConfig(data, options) { _buildChartConfig(data, options) {
return { return {
@ -162,21 +161,21 @@ export default class AdminReportChart extends Component {
}, },
}, },
}; };
} },
_resetChart() { _resetChart() {
if (this._chart) { if (this._chart) {
this._chart.destroy(); this._chart.destroy();
this._chart = null; this._chart = null;
} }
} },
_applyChartGrouping(model, data, options) { _applyChartGrouping(model, data, options) {
return Report.collapse(model, data, options.chartGrouping); return Report.collapse(model, data, options.chartGrouping);
} },
@bind @bind
_resizeHandler() { _resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500); discourseDebounce(this, this._scheduleChartRendering, 500);
} },
} });

View File

@ -1,6 +1,6 @@
import { attributeBindings, classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-counters"],
@classNames("admin-report-counters") attributeBindings: ["model.description:title"],
@attributeBindings("model.description:title") });
export default class AdminReportCounters extends Component {}

View File

@ -1,12 +1,11 @@
import { classNameBindings, tagName } from "@ember-decorators/component";
import { match } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { match } from "@ember/object/computed";
@tagName("tr") export default Component.extend({
@classNameBindings("reverseColors") allTime: true,
export default class AdminReportCounts extends Component { tagName: "tr",
allTime = true; reverseColors: match(
"report.type",
@match("report.type", /^(time_to_first_response|topics_with_no_response)$/) /^(time_to_first_response|topics_with_no_response)$/
reverseColors; ),
} classNameBindings: ["reverseColors"],
});

View File

@ -1,5 +1,4 @@
import { classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@classNames("admin-report-inline-table") classNames: ["admin-report-inline-table"],
export default class AdminReportInlineTable extends Component {} });

View File

@ -1,5 +1,4 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("tr") tagName: "tr",
export default class AdminReportPerDayCounts extends Component {} });

View File

@ -1,4 +1,3 @@
import { classNames } from "@ember-decorators/component";
import Report from "admin/models/report"; import Report from "admin/models/report";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
@ -8,31 +7,32 @@ import { number } from "discourse/lib/formatter";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
@classNames("admin-report-chart", "admin-report-stacked-chart") export default Component.extend({
export default class AdminReportStackedChart extends Component { classNames: ["admin-report-chart", "admin-report-stacked-chart"],
didInsertElement() { didInsertElement() {
super.didInsertElement(...arguments); this._super(...arguments);
window.addEventListener("resize", this._resizeHandler); window.addEventListener("resize", this._resizeHandler);
} },
willDestroyElement() { willDestroyElement() {
super.willDestroyElement(...arguments); this._super(...arguments);
window.removeEventListener("resize", this._resizeHandler); window.removeEventListener("resize", this._resizeHandler);
this._resetChart(); this._resetChart();
} },
didReceiveAttrs() { didReceiveAttrs() {
super.didReceiveAttrs(...arguments); this._super(...arguments);
discourseDebounce(this, this._scheduleChartRendering, 100); discourseDebounce(this, this._scheduleChartRendering, 100);
} },
@bind @bind
_resizeHandler() { _resizeHandler() {
discourseDebounce(this, this._scheduleChartRendering, 500); discourseDebounce(this, this._scheduleChartRendering, 500);
} },
_scheduleChartRendering() { _scheduleChartRendering() {
schedule("afterRender", () => { schedule("afterRender", () => {
@ -45,7 +45,7 @@ export default class AdminReportStackedChart extends Component {
this.element.querySelector(".chart-canvas") this.element.querySelector(".chart-canvas")
); );
}); });
} },
_renderChart(model, chartCanvas) { _renderChart(model, chartCanvas) {
if (!chartCanvas) { if (!chartCanvas) {
@ -79,7 +79,7 @@ export default class AdminReportStackedChart extends Component {
this._chart = new window.Chart(context, this._buildChartConfig(data)); this._chart = new window.Chart(context, this._buildChartConfig(data));
}); });
} },
_buildChartConfig(data) { _buildChartConfig(data) {
return { return {
@ -150,10 +150,10 @@ export default class AdminReportStackedChart extends Component {
}, },
}, },
}; };
} },
_resetChart() { _resetChart() {
this._chart?.destroy(); this._chart?.destroy();
this._chart = null; this._chart = null;
} },
} });

View File

@ -1,45 +1,43 @@
import { classNames } from "@ember-decorators/component";
import { alias } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { setting } from "discourse/lib/computed"; import { setting } from "discourse/lib/computed";
@classNames("admin-report-storage-stats") export default Component.extend({
export default class AdminReportStorageStats extends Component { classNames: ["admin-report-storage-stats"],
@setting("backup_location") backupLocation;
@alias("model.data.backups") backupStats; backupLocation: setting("backup_location"),
backupStats: alias("model.data.backups"),
@alias("model.data.uploads") uploadStats; uploadStats: alias("model.data.uploads"),
@discourseComputed("backupStats") @discourseComputed("backupStats")
showBackupStats(stats) { showBackupStats(stats) {
return stats && this.currentUser.admin; return stats && this.currentUser.admin;
} },
@discourseComputed("backupLocation") @discourseComputed("backupLocation")
backupLocationName(backupLocation) { backupLocationName(backupLocation) {
return I18n.t(`admin.backups.location.${backupLocation}`); return I18n.t(`admin.backups.location.${backupLocation}`);
} },
@discourseComputed("backupStats.used_bytes") @discourseComputed("backupStats.used_bytes")
usedBackupSpace(bytes) { usedBackupSpace(bytes) {
return I18n.toHumanSize(bytes); return I18n.toHumanSize(bytes);
} },
@discourseComputed("backupStats.free_bytes") @discourseComputed("backupStats.free_bytes")
freeBackupSpace(bytes) { freeBackupSpace(bytes) {
return I18n.toHumanSize(bytes); return I18n.toHumanSize(bytes);
} },
@discourseComputed("uploadStats.used_bytes") @discourseComputed("uploadStats.used_bytes")
usedUploadSpace(bytes) { usedUploadSpace(bytes) {
return I18n.toHumanSize(bytes); return I18n.toHumanSize(bytes);
} },
@discourseComputed("uploadStats.free_bytes") @discourseComputed("uploadStats.free_bytes")
freeUploadSpace(bytes) { freeUploadSpace(bytes) {
return I18n.toHumanSize(bytes); return I18n.toHumanSize(bytes);
} },
} });

View File

@ -1,27 +1,21 @@
import {
attributeBindings,
classNameBindings,
classNames,
tagName,
} from "@ember-decorators/component";
import { alias } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@tagName("td") export default Component.extend({
@classNames("admin-report-table-cell") tagName: "td",
@classNameBindings("type", "property") classNames: ["admin-report-table-cell"],
@attributeBindings("value:title") classNameBindings: ["type", "property"],
export default class AdminReportTableCell extends Component { attributeBindings: ["value:title"],
options = null; options: null,
@alias("label.type") type;
@alias("label.mainProperty") property;
@alias("computedLabel.formattedValue") formattedValue;
@alias("computedLabel.value") value;
@discourseComputed("label", "data", "options") @discourseComputed("label", "data", "options")
computedLabel(label, data, options) { computedLabel(label, data, options) {
return label.compute(data, options || {}); return label.compute(data, options || {});
} },
}
type: alias("label.type"),
property: alias("label.mainProperty"),
formattedValue: alias("computedLabel.formattedValue"),
value: alias("computedLabel.value"),
});

View File

@ -1,24 +1,19 @@
import {
attributeBindings,
classNameBindings,
classNames,
tagName,
} from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@tagName("th") export default Component.extend({
@classNames("admin-report-table-header") tagName: "th",
@classNameBindings("label.mainProperty", "label.type", "isCurrentSort") classNames: ["admin-report-table-header"],
@attributeBindings("label.title:title") classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
export default class AdminReportTableHeader extends Component { attributeBindings: ["label.title:title"],
@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty") @discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
isCurrentSort(currentSortField, labelSortField) { isCurrentSort(currentSortField, labelSortField) {
return currentSortField === labelSortField; return currentSortField === labelSortField;
} },
@discourseComputed("currentSortDirection") @discourseComputed("currentSortDirection")
sortIcon(currentSortDirection) { sortIcon(currentSortDirection) {
return currentSortDirection === 1 ? "caret-up" : "caret-down"; return currentSortDirection === 1 ? "caret-up" : "caret-down";
} },
} });

View File

@ -1,8 +1,6 @@
import { classNames, tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("tr") tagName: "tr",
@classNames("admin-report-table-row") classNames: ["admin-report-table-row"],
export default class AdminReportTableRow extends Component { options: null,
options = null; });
}

View File

@ -1,26 +1,22 @@
import { action } from "@ember/object";
import { classNameBindings, classNames } from "@ember-decorators/component";
import { alias } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers"; import { makeArray } from "discourse-common/lib/helpers";
const PAGES_LIMIT = 8; const PAGES_LIMIT = 8;
@classNameBindings("sortable", "twoColumns") export default Component.extend({
@classNames("admin-report-table") classNameBindings: ["sortable", "twoColumns"],
export default class AdminReportTable extends Component { classNames: ["admin-report-table"],
sortable = false; sortable: false,
sortDirection = 1; sortDirection: 1,
perPage: alias("options.perPage"),
@alias("options.perPage") perPage; page: 0,
page = 0;
@discourseComputed("model.computedLabels.length") @discourseComputed("model.computedLabels.length")
twoColumns(labelsLength) { twoColumns(labelsLength) {
return labelsLength === 2; return labelsLength === 2;
} },
@discourseComputed( @discourseComputed(
"totalsForSample", "totalsForSample",
@ -35,12 +31,12 @@ export default class AdminReportTable extends Component {
.reduce((s, v) => s + v, 0); .reduce((s, v) => s + v, 0);
return sum >= 1 && total && datesFiltering; return sum >= 1 && total && datesFiltering;
} },
@discourseComputed("model.total", "options.total", "twoColumns") @discourseComputed("model.total", "options.total", "twoColumns")
showTotal(reportTotal, total, twoColumns) { showTotal(reportTotal, total, twoColumns) {
return reportTotal && total && twoColumns; return reportTotal && total && twoColumns;
} },
@discourseComputed( @discourseComputed(
"model.{average,data}", "model.{average,data}",
@ -54,17 +50,17 @@ export default class AdminReportTable extends Component {
sampleTotalValue && sampleTotalValue &&
hasTwoColumns hasTwoColumns
); );
} },
@discourseComputed("totalsForSample.1.value", "model.data.length") @discourseComputed("totalsForSample.1.value", "model.data.length")
averageForSample(totals, count) { averageForSample(totals, count) {
return (totals / count).toFixed(0); return (totals / count).toFixed(0);
} },
@discourseComputed("model.data.length") @discourseComputed("model.data.length")
showSortingUI(dataLength) { showSortingUI(dataLength) {
return dataLength >= 5; return dataLength >= 5;
} },
@discourseComputed("totalsForSampleRow", "model.computedLabels") @discourseComputed("totalsForSampleRow", "model.computedLabels")
totalsForSample(row, labels) { totalsForSample(row, labels) {
@ -74,7 +70,7 @@ export default class AdminReportTable extends Component {
computedLabel.property = label.mainProperty; computedLabel.property = label.mainProperty;
return computedLabel; return computedLabel;
}); });
} },
@discourseComputed("model.data", "model.computedLabels") @discourseComputed("model.data", "model.computedLabels")
totalsForSampleRow(rows, labels) { totalsForSampleRow(rows, labels) {
@ -102,7 +98,7 @@ export default class AdminReportTable extends Component {
}); });
return totalsRow; return totalsRow;
} },
@discourseComputed("sortLabel", "sortDirection", "model.data.[]") @discourseComputed("sortLabel", "sortDirection", "model.data.[]")
sortedData(sortLabel, sortDirection, data) { sortedData(sortLabel, sortDirection, data) {
@ -122,7 +118,7 @@ export default class AdminReportTable extends Component {
} }
return data; return data;
} },
@discourseComputed("sortedData.[]", "perPage", "page") @discourseComputed("sortedData.[]", "perPage", "page")
paginatedData(data, perPage, page) { paginatedData(data, perPage, page) {
@ -132,7 +128,7 @@ export default class AdminReportTable extends Component {
} }
return data; return data;
} },
@discourseComputed("model.data", "perPage", "page") @discourseComputed("model.data", "perPage", "page")
pages(data, perPage, page) { pages(data, perPage, page) {
@ -160,19 +156,19 @@ export default class AdminReportTable extends Component {
} }
return pages; return pages;
} },
@action actions: {
changePage(page) { changePage(page) {
this.set("page", page); this.set("page", page);
} },
@action sortByLabel(label) {
sortByLabel(label) { if (this.sortLabel === label) {
if (this.sortLabel === label) { this.set("sortDirection", this.sortDirection === 1 ? -1 : 1);
this.set("sortDirection", this.sortDirection === 1 ? -1 : 1); } else {
} else { this.set("sortLabel", label);
this.set("sortLabel", label); }
} },
} },
} });

View File

@ -1,5 +1,4 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("tr") tagName: "tr",
export default class AdminReportTrustLevelCounts extends Component {} });

View File

@ -1,7 +1,6 @@
import { classNameBindings, classNames } from "@ember-decorators/component";
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
import EmberObject, { action, computed } from "@ember/object"; import EmberObject, { action, computed } from "@ember/object";
import Report, { DAILY_LIMIT_DAYS, SCHEMA_VERSION } from "admin/models/report"; import Report, { DAILY_LIMIT_DAYS, SCHEMA_VERSION } from "admin/models/report";
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import ReportLoader from "discourse/lib/reports-loader"; import ReportLoader from "discourse/lib/reports-loader";
@ -22,58 +21,51 @@ const TABLE_OPTIONS = {
const CHART_OPTIONS = {}; const CHART_OPTIONS = {};
@classNameBindings( export default Component.extend({
"isHidden:hidden", classNameBindings: [
"isHidden::is-visible", "isHidden:hidden",
"isEnabled", "isHidden::is-visible",
"isLoading", "isEnabled",
"dasherizedDataSourceName" "isLoading",
) "dasherizedDataSourceName",
@classNames("admin-report") ],
export default class AdminReport extends Component { classNames: ["admin-report"],
isEnabled = true; isEnabled: true,
disabledLabel = I18n.t("admin.dashboard.disabled"); disabledLabel: I18n.t("admin.dashboard.disabled"),
isLoading = false; isLoading: false,
rateLimitationString = null; rateLimitationString: null,
dataSourceName = null; dataSourceName: null,
report = null; report: null,
model = null; model: null,
reportOptions = null; reportOptions: null,
forcedModes = null; forcedModes: null,
showAllReportsLink = false; showAllReportsLink: false,
filters = null; filters: null,
showTrend = false; showTrend: false,
showHeader = true; showHeader: true,
showTitle = true; showTitle: true,
showFilteringUI = false; showFilteringUI: false,
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
endDate: null,
startDate: null,
@alias("model.dates_filtering") showDatesOptions; init() {
this._super(...arguments);
@or("showDatesOptions", "model.available_filters.length") showRefresh; this._reports = [];
},
@and("showTrend", "model.prev_period") shouldDisplayTrend; isHidden: computed("siteSettings.dashboard_hidden_reports", function () {
endDate = null;
startDate = null;
@or("showTimeoutError", "showExceptionError", "showNotFoundError") showError;
@equal("model.error", "not_found") showNotFoundError;
@equal("model.error", "timeout") showTimeoutError;
@equal("model.error", "exception") showExceptionError;
@notEmpty("model.data") hasData;
_reports = [];
@computed("siteSettings.dashboard_hidden_reports")
get isHidden() {
return (this.siteSettings.dashboard_hidden_reports || "") return (this.siteSettings.dashboard_hidden_reports || "")
.split("|") .split("|")
.filter(Boolean) .filter(Boolean)
.includes(this.dataSourceName); .includes(this.dataSourceName);
} }),
didReceiveAttrs() { didReceiveAttrs() {
super.didReceiveAttrs(...arguments); this._super(...arguments);
let startDate = moment(); let startDate = moment();
if (this.filters && isPresent(this.filters.startDate)) { if (this.filters && isPresent(this.filters.startDate)) {
@ -96,35 +88,42 @@ export default class AdminReport extends Component {
} else if (this.dataSourceName) { } else if (this.dataSourceName) {
this._fetchReport(); 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") @discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) { dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-"); return (dataSourceName || type || "undefined").replace(/_/g, "-");
} },
@discourseComputed("dataSourceName", "model.type") @discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) { dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type; dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`; return `/admin/reports/${dataSourceName}`;
} },
@discourseComputed("displayedModes.length") @discourseComputed("displayedModes.length")
showModes(displayedModesLength) { showModes(displayedModesLength) {
return displayedModesLength > 1; return displayedModesLength > 1;
} },
@discourseComputed("currentMode") @discourseComputed("currentMode")
isChartMode(currentMode) { isChartMode(currentMode) {
return currentMode === "chart"; return currentMode === "chart";
} },
@action @action
changeGrouping(grouping) { changeGrouping(grouping) {
this.send("refreshReport", { this.send("refreshReport", {
chartGrouping: grouping, chartGrouping: grouping,
}); });
} },
@discourseComputed("currentMode", "model.modes", "forcedModes") @discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) { displayedModes(currentMode, reportModes, forcedModes) {
@ -140,12 +139,12 @@ export default class AdminReport extends Component {
icon: mode === "table" ? "table" : "signal", icon: mode === "table" ? "table" : "signal",
}; };
}); });
} },
@discourseComputed("currentMode") @discourseComputed("currentMode")
modeComponent(currentMode) { modeComponent(currentMode) {
return `admin-report-${currentMode.replace(/_/g, "-")}`; return `admin-report-${currentMode.replace(/_/g, "-")}`;
} },
@discourseComputed( @discourseComputed(
"dataSourceName", "dataSourceName",
@ -179,7 +178,7 @@ export default class AdminReport extends Component {
.join(":"); .join(":");
return reportKey; return reportKey;
} },
@discourseComputed("options.chartGrouping", "model.chartData.length") @discourseComputed("options.chartGrouping", "model.chartData.length")
chartGroupings(grouping, count) { chartGroupings(grouping, count) {
@ -193,7 +192,7 @@ export default class AdminReport extends Component {
class: `chart-grouping ${grouping === id ? "active" : "inactive"}`, class: `chart-grouping ${grouping === id ? "active" : "inactive"}`,
}; };
}); });
} },
@action @action
onChangeDateRange(range) { onChangeDateRange(range) {
@ -201,7 +200,7 @@ export default class AdminReport extends Component {
startDate: range.from, startDate: range.from,
endDate: range.to, endDate: range.to,
}); });
} },
@action @action
applyFilter(id, value) { applyFilter(id, value) {
@ -216,7 +215,7 @@ export default class AdminReport extends Component {
this.send("refreshReport", { this.send("refreshReport", {
filters: customFilters, filters: customFilters,
}); });
} },
@action @action
refreshReport(options = {}) { refreshReport(options = {}) {
@ -239,7 +238,7 @@ export default class AdminReport extends Component {
? this.get("filters.customFilters") ? this.get("filters.customFilters")
: options.filters, : options.filters,
}); });
} },
@action @action
exportCsv() { exportCsv() {
@ -255,7 +254,7 @@ export default class AdminReport extends Component {
} }
exportEntity("report", args).then(outputExportResult); exportEntity("report", args).then(outputExportResult);
} },
@action @action
onChangeMode(mode) { onChangeMode(mode) {
@ -264,7 +263,7 @@ export default class AdminReport extends Component {
this.send("refreshReport", { this.send("refreshReport", {
chartGrouping: null, chartGrouping: null,
}); });
} },
_computeReport() { _computeReport() {
if (!this.element || this.isDestroying || this.isDestroyed) { if (!this.element || this.isDestroying || this.isDestroyed) {
@ -307,7 +306,7 @@ export default class AdminReport extends Component {
} }
this._renderReport(report, this.forcedModes, this.currentMode); this._renderReport(report, this.forcedModes, this.currentMode);
} },
_renderReport(report, forcedModes, currentMode) { _renderReport(report, forcedModes, currentMode) {
const modes = forcedModes ? forcedModes.split(",") : report.modes; const modes = forcedModes ? forcedModes.split(",") : report.modes;
@ -318,9 +317,11 @@ export default class AdminReport extends Component {
currentMode, currentMode,
options: this._buildOptions(currentMode, report), options: this._buildOptions(currentMode, report),
}); });
} },
_fetchReport() { _fetchReport() {
this._super(...arguments);
this.setProperties({ isLoading: true, rateLimitationString: null }); this.setProperties({ isLoading: true, rateLimitationString: null });
next(() => { next(() => {
@ -348,7 +349,7 @@ export default class AdminReport extends Component {
ReportLoader.enqueue(this.dataSourceName, payload.data, callback); ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
}); });
} },
_buildPayload(facets) { _buildPayload(facets) {
let payload = { data: { facets } }; let payload = { data: { facets } };
@ -374,7 +375,7 @@ export default class AdminReport extends Component {
} }
return payload; return payload;
} },
_buildOptions(mode, report) { _buildOptions(mode, report) {
if (mode === "table") { if (mode === "table") {
@ -392,7 +393,7 @@ export default class AdminReport extends Component {
}) })
); );
} }
} },
_loadReport(jsonReport) { _loadReport(jsonReport) {
Report.fillMissingDates(jsonReport, { filledField: "chartData" }); Report.fillMissingDates(jsonReport, { filledField: "chartData" });
@ -422,5 +423,5 @@ export default class AdminReport extends Component {
} }
return Report.create(jsonReport); return Report.create(jsonReport);
} },
} });

View File

@ -1,130 +0,0 @@
<div class="edit-main-nav admin-controls">
<nav>
<ul class="nav nav-pills target">
{{#each this.visibleTargets as |target|}}
<li>
<LinkTo
@route={{this.editRouteName}}
@models={{array this.theme.id target.name this.fieldName}}
@replace={{true}}
title={{this.field.title}}
class={{if target.edited "edited" "blank"}}
>
{{#if target.error}}{{d-icon "exclamation-triangle"}}{{/if}}
{{#if target.icon}}{{d-icon target.icon}}{{/if}}
{{i18n (concat "admin.customize.theme." target.name)}}
</LinkTo>
</li>
{{/each}}
{{#if this.allowAdvanced}}
<li>
<a
{{on "click" this.toggleShowAdvanced}}
href
title={{i18n
(concat
"admin.customize.theme."
(if this.showAdvanced "hide_advanced" "show_advanced")
)
}}
class="no-text"
>
{{d-icon
(if this.showAdvanced "angle-double-left" "angle-double-right")
}}
</a>
</li>
{{/if}}
<li class="spacer"></li>
<li>
<label>
<Input
@type="checkbox"
@checked={{this.onlyOverridden}}
{{on
"click"
(action this.onlyOverriddenChanged value="target.checked")
}}
/>
{{i18n "admin.customize.theme.hide_unused_fields"}}
</label>
</li>
</ul>
</nav>
</div>
<div class="admin-controls">
<nav>
<ul class="nav nav-pills fields">
{{#each this.visibleFields as |field|}}
<li>
<LinkTo
@route={{this.editRouteName}}
@models={{array this.theme.id this.currentTargetName field.name}}
@replace={{true}}
title={{field.title}}
class={{if field.edited "edited" "blank"}}
>
{{#if field.error}}{{d-icon "exclamation-triangle"}}{{/if}}
{{#if field.icon}}{{d-icon field.icon}}{{/if}}
{{field.translatedName}}
</LinkTo>
</li>
{{/each}}
{{#if this.showAddField}}
<li>
{{#if this.addingField}}
<Input
@type={{this.text}}
@value={{this.newFieldName}}
@enter={{action "addField"}}
@escape-press={{action "cancelAddField"}}
/>
<DButton
@class="ok"
@action={{action "addField" this.newFieldName}}
@icon="check"
/>
<DButton
@class="cancel"
@action={{action "cancelAddField"}}
@icon="times"
/>
{{else}}
<a href {{on "click" this.toggleAddField}} class="no-text">
{{d-icon "plus"}}
</a>
{{/if}}
</li>
{{/if}}
<li class="spacer"></li>
<li>
<a href {{on "click" this.toggleMaximize}} class="no-text">
{{d-icon this.maximizeIcon}}
</a>
</li>
</ul>
</nav>
</div>
{{#if this.error}}
<pre class="field-error">{{this.error}}</pre>
{{/if}}
{{#if this.warning}}
<pre class="field-warning">{{html-safe this.warning}}</pre>
{{/if}}
<AceEditor
@content={{this.activeSection}}
@editorId={{this.editorId}}
@mode={{this.activeSectionMode}}
@autofocus="true"
@placeholder={{this.placeholder}}
@htmlPlaceholder={{true}}
@save={{this.save}}
@setWarning={{action "setWarning"}}
/>

View File

@ -3,13 +3,11 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed"; import { fmt } from "discourse/lib/computed";
import { isDocumentRTL } from "discourse/lib/text-direction"; import { isDocumentRTL } from "discourse/lib/text-direction";
import { action, computed } from "@ember/object"; import { action } from "@ember/object";
import { next } from "@ember/runloop"; import { next } from "@ember/runloop";
export default class AdminThemeEditor extends Component { export default Component.extend({
warning = null; warning: null,
@fmt("fieldName", "currentTargetName", "%@|%@") editorId;
@discourseComputed("theme.targets", "onlyOverridden", "showAdvanced") @discourseComputed("theme.targets", "onlyOverridden", "showAdvanced")
visibleTargets(targets, onlyOverridden, showAdvanced) { visibleTargets(targets, onlyOverridden, showAdvanced) {
@ -22,7 +20,7 @@ export default class AdminThemeEditor extends Component {
} }
return target.edited; return target.edited;
}); });
} },
@discourseComputed("currentTargetName", "onlyOverridden", "theme.fields") @discourseComputed("currentTargetName", "onlyOverridden", "theme.fields")
visibleFields(targetName, onlyOverridden, fields) { visibleFields(targetName, onlyOverridden, fields) {
@ -31,7 +29,7 @@ export default class AdminThemeEditor extends Component {
fields = fields.filter((field) => field.edited); fields = fields.filter((field) => field.edited);
} }
return fields; return fields;
} },
@discourseComputed("currentTargetName", "fieldName") @discourseComputed("currentTargetName", "fieldName")
activeSectionMode(targetName, fieldName) { activeSectionMode(targetName, fieldName) {
@ -45,7 +43,7 @@ export default class AdminThemeEditor extends Component {
return "scss"; return "scss";
} }
return fieldName && fieldName.includes("scss") ? "scss" : "html"; return fieldName && fieldName.includes("scss") ? "scss" : "html";
} },
@discourseComputed("currentTargetName", "fieldName") @discourseComputed("currentTargetName", "fieldName")
placeholder(targetName, fieldName) { placeholder(targetName, fieldName) {
@ -60,27 +58,30 @@ export default class AdminThemeEditor extends Component {
}); });
} }
return ""; return "";
} },
@computed("fieldName", "currentTargetName", "theme") @discourseComputed("fieldName", "currentTargetName", "theme")
get activeSection() { activeSection: {
return this.theme.getField(this.currentTargetName, this.fieldName); get(fieldName, target, model) {
} return model.getField(target, fieldName);
},
set(value, fieldName, target, model) {
model.setField(target, fieldName, value);
return value;
},
},
set activeSection(value) { editorId: fmt("fieldName", "currentTargetName", "%@|%@"),
this.theme.setField(this.currentTargetName, this.fieldName, value);
return value;
}
@discourseComputed("maximized") @discourseComputed("maximized")
maximizeIcon(maximized) { maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand"; return maximized ? "discourse-compress" : "discourse-expand";
} },
@discourseComputed("currentTargetName", "theme.targets") @discourseComputed("currentTargetName", "theme.targets")
showAddField(currentTargetName, targets) { showAddField(currentTargetName, targets) {
return targets.find((t) => t.name === currentTargetName).customNames; return targets.find((t) => t.name === currentTargetName).customNames;
} },
@discourseComputed( @discourseComputed(
"currentTargetName", "currentTargetName",
@ -89,45 +90,52 @@ export default class AdminThemeEditor extends Component {
) )
error(target, fieldName) { error(target, fieldName) {
return this.theme.getError(target, fieldName); return this.theme.getError(target, fieldName);
} },
@action @action
toggleShowAdvanced(event) { toggleShowAdvanced(event) {
event?.preventDefault(); event?.preventDefault();
this.toggleProperty("showAdvanced"); this.toggleProperty("showAdvanced");
} },
@action @action
toggleAddField(event) { toggleAddField(event) {
event?.preventDefault(); event?.preventDefault();
this.toggleProperty("addingField"); this.toggleProperty("addingField");
} },
@action @action
toggleMaximize(event) { toggleMaximize(event) {
event?.preventDefault(); event?.preventDefault();
this.toggleProperty("maximized"); this.toggleProperty("maximized");
next(() => this.appEvents.trigger("ace:resize")); next(() => this.appEvents.trigger("ace:resize"));
} },
@action actions: {
cancelAddField() { cancelAddField() {
this.set("addingField", false); this.set("addingField", false);
} },
@action addField(name) {
addField(name) { if (!name) {
if (!name) { return;
return; }
} name = name.replace(/[^a-zA-Z0-9-_/]/g, "");
name = name.replace(/[^a-zA-Z0-9-_/]/g, ""); this.theme.setField(this.currentTargetName, name, "");
this.theme.setField(this.currentTargetName, name, ""); this.setProperties({ newFieldName: "", addingField: false });
this.setProperties({ newFieldName: "", addingField: false }); this.fieldAdded(this.currentTargetName, name);
this.fieldAdded(this.currentTargetName, name); },
}
@action onlyOverriddenChanged(value) {
setWarning(message) { this.onlyOverriddenChanged(value);
this.set("warning", message); },
}
} save() {
this.attrs.save();
},
setWarning(message) {
this.set("warning", message);
},
},
});

View File

@ -1,27 +1,23 @@
import { classNames } from "@ember-decorators/component";
import { inject as service } from "@ember/service";
import { alias, equal } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { alias, equal } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { action } from "@ember/object"; import { action } from "@ember/object";
import I18n from "I18n"; import I18n from "I18n";
import { inject as service } from "@ember/service";
@classNames("watched-word") export default Component.extend({
export default class AdminWatchedWord extends Component { classNames: ["watched-word"],
@service dialog; dialog: service(),
@equal("actionKey", "replace") isReplace; isReplace: equal("actionKey", "replace"),
isTag: equal("actionKey", "tag"),
@equal("actionKey", "tag") isTag; isLink: equal("actionKey", "link"),
isCaseSensitive: alias("word.case_sensitive"),
@equal("actionKey", "link") isLink;
@alias("word.case_sensitive") isCaseSensitive;
@discourseComputed("word.replacement") @discourseComputed("word.replacement")
tags(replacement) { tags(replacement) {
return replacement.split(","); return replacement.split(",");
} },
@action @action
deleteWord() { deleteWord() {
@ -37,5 +33,5 @@ export default class AdminWatchedWord extends Component {
}) })
); );
}); });
} },
} });

View File

@ -1,15 +1,14 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
export default class AdminWrapper extends Component {
didInsertElement() { didInsertElement() {
super.didInsertElement(...arguments); this._super(...arguments);
document.querySelector("html").classList.add("admin-area"); document.querySelector("html").classList.add("admin-area");
document.querySelector("body").classList.add("admin-interface"); document.querySelector("body").classList.add("admin-interface");
} },
willDestroyElement() { willDestroyElement() {
super.willDestroyElement(...arguments); this._super(...arguments);
document.querySelector("html").classList.remove("admin-area"); document.querySelector("html").classList.remove("admin-area");
document.querySelector("body").classList.remove("admin-interface"); document.querySelector("body").classList.remove("admin-interface");
} },
} });

View File

@ -1,5 +1,4 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("") tagName: "",
export default class CancelLink extends Component {} });

View File

@ -1,7 +1,6 @@
import { classNames } from "@ember-decorators/component";
import { action, computed } from "@ember/object"; import { action, computed } from "@ember/object";
import Component from "@ember/component"; import Component from "@ember/component";
import { observes } from "@ember-decorators/object"; import { observes } from "discourse-common/utils/decorators";
/** /**
An input field for a color. An input field for a color.
@ -10,20 +9,20 @@ import { observes } from "@ember-decorators/object";
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor. @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. @params valid is a boolean indicating if the input field is a valid color.
**/ **/
@classNames("color-picker") export default Component.extend({
export default class ColorInput extends Component { classNames: ["color-picker"],
onlyHex = true;
styleSelection = true;
@computed("onlyHex") onlyHex: true,
get maxlength() {
styleSelection: true,
maxlength: computed("onlyHex", function () {
return this.onlyHex ? 6 : null; return this.onlyHex ? 6 : null;
} }),
@computed("hexValue") normalizedHexValue: computed("hexValue", function () {
get normalizedHexValue() {
return this.normalize(this.hexValue); return this.normalize(this.hexValue);
} }),
normalize(color) { normalize(color) {
if (this._valid(color)) { if (this._valid(color)) {
@ -41,19 +40,19 @@ export default class ColorInput extends Component {
} }
} }
return color; return color;
} },
@action @action
onHexInput(color) { onHexInput(color) {
if (this.attrs.onChangeColor) { if (this.attrs.onChangeColor) {
this.attrs.onChangeColor(this.normalize(color || "")); this.attrs.onChangeColor(this.normalize(color || ""));
} }
} },
@action @action
onPickerInput(event) { onPickerInput(event) {
this.set("hexValue", event.target.value.replace("#", "")); this.set("hexValue", event.target.value.replace("#", ""));
} },
@observes("hexValue", "brightnessValue", "valid") @observes("hexValue", "brightnessValue", "valid")
hexValueChanged() { hexValueChanged() {
@ -66,9 +65,9 @@ export default class ColorInput extends Component {
if (this._valid()) { if (this._valid()) {
this.element.querySelector(".picker").value = this.normalize(hex); this.element.querySelector(".picker").value = this.normalize(hex);
} }
} },
_valid(color = this.hexValue) { _valid(color = this.hexValue) {
return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color); return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color);
} },
} });

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class DashboardNewFeatureItem extends Component {} export default Component.extend({});

View File

@ -1,19 +1,18 @@
import { classNameBindings, classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import { action, computed } from "@ember/object"; import { action, computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
@classNames("section", "dashboard-new-features") export default Component.extend({
@classNameBindings("hasUnseenFeatures:ordered-first") newFeatures: null,
export default class DashboardNewFeatures extends Component { classNames: ["section", "dashboard-new-features"],
newFeatures = null; classNameBindings: ["hasUnseenFeatures:ordered-first"],
releaseNotesLink = null; releaseNotesLink: null,
constructor() { init() {
super(...arguments); this._super(...arguments);
ajax("/admin/dashboard/new-features.json").then((json) => { ajax("/admin/dashboard/new-features.json").then((json) => {
if (this.isDestroying || this.isDestroyed) { if (!this.element || this.isDestroying || this.isDestroyed) {
return; return;
} }
@ -23,17 +22,16 @@ export default class DashboardNewFeatures extends Component {
releaseNotesLink: json.release_notes_link, releaseNotesLink: json.release_notes_link,
}); });
}); });
} },
@computed("newFeatures") columnCountClass: computed("newFeatures", function () {
get columnCountClass() {
return this.newFeatures.length > 2 ? "three-or-more-items" : ""; return this.newFeatures.length > 2 ? "three-or-more-items" : "";
} }),
@action @action
dismissNewFeatures() { dismissNewFeatures() {
ajax("/admin/dashboard/mark-new-features-as-seen.json", { ajax("/admin/dashboard/mark-new-features-as-seen.json", {
type: "PUT", type: "PUT",
}).then(() => this.set("hasUnseenFeatures", false)); }).then(() => this.set("hasUnseenFeatures", false));
} },
} });

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class DashboardProblems extends Component {} export default Component.extend({});

View File

@ -1,44 +0,0 @@
<div class="row">
<div class="admin-controls">
<nav>
<ul class="nav nav-pills">
<li>
<LinkTo
@route="adminCustomizeEmailStyle.edit"
@model="html"
@replace={{true}}
>
{{i18n "admin.customize.email_style.html"}}
</LinkTo>
</li>
<li>
<LinkTo
@route="adminCustomizeEmailStyle.edit"
@model="css"
@replace={{true}}
>
{{i18n "admin.customize.email_style.css"}}
</LinkTo>
</li>
</ul>
</nav>
</div>
</div>
<AceEditor
@content={{this.editorContents}}
@mode={{this.currentEditorMode}}
@editorId={{this.editorId}}
@save={{@save}}
/>
<div class="admin-footer">
<div class="buttons">
<DButton
@action={{action "reset"}}
@disabled={{this.resetDisabled}}
@class="btn-default"
@label="admin.customize.email_style.reset"
/>
</div>
</div>

View File

@ -1,19 +1,17 @@
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import { reads } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import { inject as service } from "@ember/service";
export default class EmailStylesEditor extends Component { export default Component.extend({
@service dialog; dialog: service(),
editorId: reads("fieldName"),
@reads("fieldName") editorId;
@discourseComputed("fieldName") @discourseComputed("fieldName")
currentEditorMode(fieldName) { currentEditorMode(fieldName) {
return fieldName === "css" ? "scss" : fieldName; return fieldName === "css" ? "scss" : fieldName;
} },
@discourseComputed("fieldName", "styles.html", "styles.css") @discourseComputed("fieldName", "styles.html", "styles.css")
resetDisabled(fieldName) { resetDisabled(fieldName) {
@ -21,31 +19,36 @@ export default class EmailStylesEditor extends Component {
this.get(`styles.${fieldName}`) === this.get(`styles.${fieldName}`) ===
this.get(`styles.default_${fieldName}`) this.get(`styles.default_${fieldName}`)
); );
} },
@computed("styles", "fieldName") @discourseComputed("styles", "fieldName")
get editorContents() { editorContents: {
return this.styles[this.fieldName]; get(styles, fieldName) {
} return styles[fieldName];
},
set(value, styles, fieldName) {
styles.setField(fieldName, value);
return value;
},
},
set editorContents(value) { actions: {
this.styles.setField(this.fieldName, value); reset() {
return value; this.dialog.yesNoConfirm({
} message: I18n.t("admin.customize.email_style.reset_confirm", {
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`),
@action }),
reset() { didConfirm: () => {
this.dialog.yesNoConfirm({ this.styles.setField(
message: I18n.t("admin.customize.email_style.reset_confirm", { this.fieldName,
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`), this.styles.get(`default_${this.fieldName}`)
}), );
didConfirm: () => { this.notifyPropertyChange("editorContents");
this.styles.setField( },
this.fieldName, });
this.styles.get(`default_${this.fieldName}`) },
); save() {
this.notifyPropertyChange("editorContents"); this.attrs.save();
}, },
}); },
} });
}

View File

@ -1,66 +0,0 @@
{{#if this.editing}}
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.host"}}</div>
<Input
@value={{this.buffered.host}}
placeholder="example.com"
@enter={{action "save"}}
class="host-name"
autofocus={{true}}
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.allowed_paths"}}</div>
<Input
@value={{this.buffered.allowed_paths}}
placeholder="/blog/.*"
@enter={{action "save"}}
class="path-allowlist"
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.category"}}</div>
<CategoryChooser
@value={{this.categoryId}}
@class="small"
@onChange={{action (mut this.categoryId)}}
/>
</td>
<td class="editing-controls">
<DButton
@icon="check"
@action={{action "save"}}
@class="btn-primary"
@disabled={{this.cantSave}}
/>
<DButton
@icon="times"
@action={{action "cancel"}}
@class="btn-danger"
@disabled={{this.host.isSaving}}
/>
</td>
{{else}}
<td>
<div class="label">{{i18n "admin.embedding.host"}}</div>
{{this.host.host}}
</td>
<td>
<div class="label">
{{i18n "admin.embedding.allowed_paths"}}
</div>
{{this.host.allowed_paths}}
</td>
<td>
<div class="label">{{i18n "admin.embedding.category"}}</div>
{{category-badge this.host.category allowUncategorized=true}}
</td>
<td class="controls">
<DButton @icon="pencil-alt" @action={{action "edit"}} />
<DButton
@icon="far-trash-alt"
@action={{action "delete"}}
@class="btn-danger"
/>
</td>
{{/if}}

View File

@ -1,91 +1,85 @@
import { action } from "@ember/object";
import { tagName } from "@ember-decorators/component";
import { inject as service } from "@ember/service";
import { or } from "@ember/object/computed";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import { bufferedProperty } from "discourse/mixins/buffered-content"; import { bufferedProperty } from "discourse/mixins/buffered-content";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
@tagName("tr") export default Component.extend(bufferedProperty("host"), {
export default class EmbeddableHost extends Component.extend( editToggled: false,
bufferedProperty("host") tagName: "tr",
) { categoryId: null,
@service dialog; category: null,
editToggled = false; dialog: service(),
categoryId = null;
category = null;
@or("host.isNew", "editToggled") editing; editing: or("host.isNew", "editToggled"),
init() { init() {
super.init(...arguments); this._super(...arguments);
const host = this.host; const host = this.host;
const categoryId = host.category_id || this.site.uncategorized_category_id; const categoryId = host.category_id || this.site.uncategorized_category_id;
const category = Category.findById(categoryId); const category = Category.findById(categoryId);
host.set("category", category); host.set("category", category);
} },
@discourseComputed("buffered.host", "host.isSaving") @discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) { cantSave(host, isSaving) {
return isSaving || isEmpty(host); return isSaving || isEmpty(host);
} },
@action actions: {
edit() { edit() {
this.set("categoryId", this.get("host.category.id")); this.set("categoryId", this.get("host.category.id"));
this.set("editToggled", true); this.set("editToggled", true);
} },
@action save() {
save() { if (this.cantSave) {
if (this.cantSave) { return;
return; }
}
const props = this.buffered.getProperties( const props = this.buffered.getProperties(
"host", "host",
"allowed_paths", "allowed_paths",
"class_name" "class_name"
); );
props.category_id = this.categoryId; props.category_id = this.categoryId;
const host = this.host; const host = this.host;
host host
.save(props) .save(props)
.then(() => { .then(() => {
host.set("category", Category.findById(this.categoryId)); host.set("category", Category.findById(this.categoryId));
this.set("editToggled", false);
})
.catch(popupAjaxError);
},
delete() {
return this.dialog.confirm({
message: I18n.t("admin.embedding.confirm_delete"),
didConfirm: () => {
return 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); this.set("editToggled", false);
}) }
.catch(popupAjaxError); },
} },
});
@action
delete() {
return this.dialog.confirm({
message: I18n.t("admin.embedding.confirm_delete"),
didConfirm: () => {
return this.host.destroyRecord().then(() => {
this.deleteHost(this.host);
});
},
});
}
@action
cancel() {
const host = this.host;
if (host.get("isNew")) {
this.deleteHost(host);
} else {
this.rollbackBuffer();
this.set("editToggled", false);
}
}
}

View File

@ -1,33 +1,33 @@
import { classNames } from "@ember-decorators/component";
import { computed } from "@ember/object";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { dasherize } from "@ember/string"; import { dasherize } from "@ember/string";
@classNames("embed-setting") export default Component.extend({
export default class EmbeddingSetting extends Component { classNames: ["embed-setting"],
@discourseComputed("field") @discourseComputed("field")
inputId(field) { inputId(field) {
return dasherize(field); return dasherize(field);
} },
@discourseComputed("field") @discourseComputed("field")
translationKey(field) { translationKey(field) {
return `admin.embedding.${field}`; return `admin.embedding.${field}`;
} },
@discourseComputed("type") @discourseComputed("type")
isCheckbox(type) { isCheckbox(type) {
return type === "checkbox"; return type === "checkbox";
} },
@computed("value") @discourseComputed("value")
get checked() { checked: {
return !!this.value; get(value) {
} return !!value;
},
set(value) { set(value) {
this.set("value", value); this.set("value", value);
return value; return value;
} },
} },
});

View File

@ -1,4 +1,3 @@
import { classNameBindings } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@ -7,12 +6,12 @@ import { action, set, setProperties } from "@ember/object";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
@classNameBindings(":value-list", ":emoji-list") export default Component.extend({
export default class EmojiValueList extends Component { classNameBindings: [":value-list", ":emoji-list"],
values = null; values: null,
validationMessage = null; validationMessage: null,
emojiPickerIsActive = false; emojiPickerIsActive: false,
isEditorFocused = false; isEditorFocused: false,
@discourseComputed("values") @discourseComputed("values")
collection(values) { collection(values) {
@ -29,14 +28,14 @@ export default class EmojiValueList extends Component {
emojiUrl: emojiUrlFor(value), emojiUrl: emojiUrlFor(value),
}; };
}); });
} },
@action @action
closeEmojiPicker() { closeEmojiPicker() {
this.collection.setEach("isEditing", false); this.collection.setEach("isEditing", false);
this.set("emojiPickerIsActive", false); this.set("emojiPickerIsActive", false);
this.set("isEditorFocused", false); this.set("isEditorFocused", false);
} },
@action @action
emojiSelected(code) { emojiSelected(code) {
@ -66,12 +65,12 @@ export default class EmojiValueList extends Component {
this.set("emojiPickerIsActive", false); this.set("emojiPickerIsActive", false);
this.set("isEditorFocused", false); this.set("isEditorFocused", false);
} },
@discourseComputed("collection") @discourseComputed("collection")
showUpDownButtons(collection) { showUpDownButtons(collection) {
return collection.length - 1 ? true : false; return collection.length - 1 ? true : false;
} },
_splitValues(values) { _splitValues(values) {
if (values && values.length) { if (values && values.length) {
@ -92,7 +91,7 @@ export default class EmojiValueList extends Component {
} else { } else {
return []; return [];
} }
} },
@action @action
editValue(index) { editValue(index) {
@ -112,12 +111,12 @@ export default class EmojiValueList extends Component {
} }
}, 100); }, 100);
}); });
} },
@action @action
removeValue(value) { removeValue(value) {
this._removeValue(value); this._removeValue(value);
} },
@action @action
shift(operation, index) { shift(operation, index) {
@ -134,7 +133,7 @@ export default class EmojiValueList extends Component {
this.collection.insertAt(futureIndex, shiftedEmoji); this.collection.insertAt(futureIndex, shiftedEmoji);
this._saveValues(); this._saveValues();
} },
_validateInput(input) { _validateInput(input) {
this.set("validationMessage", null); this.set("validationMessage", null);
@ -148,12 +147,12 @@ export default class EmojiValueList extends Component {
} }
return true; return true;
} },
_removeValue(value) { _removeValue(value) {
this.collection.removeObject(value); this.collection.removeObject(value);
this._saveValues(); this._saveValues();
} },
_replaceValue(index, newValue) { _replaceValue(index, newValue) {
const item = this.collection[index]; const item = this.collection[index];
@ -162,9 +161,9 @@ export default class EmojiValueList extends Component {
} }
set(item, "value", newValue); set(item, "value", newValue);
this._saveValues(); this._saveValues();
} },
_saveValues() { _saveValues() {
this.set("values", this.collection.mapBy("value").join("|")); this.set("values", this.collection.mapBy("value").join("|"));
} },
} });

View File

@ -1,5 +1,4 @@
import { classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@classNames("flag-user-lists") classNames: ["flag-user-lists"],
export default class FlagUserLists extends Component {} });

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class FlagUser extends Component {} export default Component.extend({});

View File

@ -1,71 +0,0 @@
<div class="form-templates__form">
<div class="control-group">
<label for="template-name">
{{i18n "admin.form_templates.new_template_form.name.label"}}
</label>
<TextField
@value={{this.templateName}}
@name="template-name"
@class="form-templates__form-name-input"
@placeholderKey="admin.form_templates.new_template_form.name.placeholder"
/>
</div>
<div class="control-group form-templates__editor">
<div class="form-templates__quick-insert-field-buttons">
<span>
{{I18n "admin.form_templates.quick_insert_fields.add_new_field"}}
</span>
{{#each this.quickInsertFields as |field|}}
<DButton
@class="btn-flat btn-icon-text quick-insert-{{field.type}}"
@icon={{field.icon}}
@label="admin.form_templates.quick_insert_fields.{{field.type}}"
@action={{this.onInsertField}}
@actionParam={{field.type}}
/>
{{/each}}
<DButton
class="btn-flat btn-icon-text form-templates__validations-modal-button"
@label="admin.form_templates.validations_modal.button_title"
@icon="check-circle"
@action={{this.showValidationOptionsModal}}
/>
</div>
<DButton
@class="form-templates__preview-button"
@icon="eye"
@label="admin.form_templates.new_template_form.preview"
@action={{this.showPreview}}
@disabled={{this.disablePreviewButton}}
/>
</div>
<div class="control-group">
<AceEditor @content={{this.templateContent}} @mode="yaml" />
</div>
<div class="footer-buttons">
<DButton
@class="btn-primary"
@label="admin.form_templates.new_template_form.submit"
@icon="check"
@action={{this.onSubmit}}
@disabled={{this.disableSubmitButton}}
/>
<DButton
@label="admin.form_templates.new_template_form.cancel"
@icon="times"
@action={{this.onCancel}}
/>
{{#if this.isEditing}}
<DButton
@class="btn-danger"
@label="admin.form_templates.view_template.delete"
@icon="trash-alt"
@action={{this.onDelete}}
/>
{{/if}}
</div>
</div>

View File

@ -1,140 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { templateFormFields } from "admin/lib/template-form-fields";
import FormTemplate from "admin/models/form-template";
import showModal from "discourse/lib/show-modal";
export default class FormTemplateForm extends Component {
@service router;
@service dialog;
@tracked formSubmitted = false;
@tracked templateContent = this.args.model?.template || "";
@tracked templateName = this.args.model?.name || "";
isEditing = this.args.model?.id ? true : false;
quickInsertFields = [
{
type: "checkbox",
icon: "check-square",
},
{
type: "input",
icon: "grip-lines",
},
{
type: "textarea",
icon: "align-left",
},
{
type: "dropdown",
icon: "chevron-circle-down",
},
{
type: "upload",
icon: "cloud-upload-alt",
},
{
type: "multiselect",
icon: "bullseye",
},
];
get disablePreviewButton() {
return Boolean(!this.templateName.length || !this.templateContent.length);
}
get disableSubmitButton() {
return (
Boolean(!this.templateName.length || !this.templateContent.length) ||
this.formSubmitted
);
}
@action
onSubmit() {
if (!this.formSubmitted) {
this.formSubmitted = true;
}
const postData = {
name: this.templateName,
template: this.templateContent,
};
if (this.isEditing) {
postData["id"] = this.args.model.id;
}
FormTemplate.createOrUpdateTemplate(postData)
.then(() => {
this.formSubmitted = false;
this.router.transitionTo("adminCustomizeFormTemplates.index");
})
.catch((e) => {
popupAjaxError(e);
this.formSubmitted = false;
});
}
@action
onCancel() {
this.router.transitionTo("adminCustomizeFormTemplates.index");
}
@action
onDelete() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.form_templates.delete_confirm"),
didConfirm: () => {
FormTemplate.deleteTemplate(this.args.model.id)
.then(() => {
this.router.transitionTo("adminCustomizeFormTemplates.index");
})
.catch(popupAjaxError);
},
});
}
@action
onInsertField(type) {
const structure = templateFormFields.findBy("type", type).structure;
if (this.templateContent.length === 0) {
this.templateContent += structure;
} else {
this.templateContent += `\n${structure}`;
}
}
@action
showValidationOptionsModal() {
return showModal("admin-form-template-validation-options", {
admin: true,
});
}
@action
showPreview() {
const data = {
name: this.templateName,
template: this.templateContent,
};
if (this.isEditing) {
data["id"] = this.args.model.id;
}
FormTemplate.validateTemplate(data)
.then(() => {
return showModal("form-template-form-preview", {
model: {
content: this.templateContent,
},
});
})
.catch(popupAjaxError);
}
}

View File

@ -1,4 +0,0 @@
<div class="form-templates--info">
<h2>{{i18n "admin.form_templates.title"}}</h2>
<p class="desc">{{i18n "admin.form_templates.help"}}</p>
</div>

View File

@ -1,28 +0,0 @@
<tr class="admin-list-item">
<td class="col first">{{@template.name}}</td>
<td class="col categories">
{{#each this.activeCategories as |category|}}
{{category-link category}}
{{/each}}
</td>
<td class="col action">
<DButton
@title="admin.form_templates.list_table.actions.view"
@icon="far-eye"
@class="btn-view-template"
@action={{this.viewTemplate}}
/>
<DButton
@title="admin.form_templates.list_table.actions.edit"
@icon="pencil-alt"
@class="btn-edit-template"
@action={{this.editTemplate}}
/>
<DButton
@title="admin.form_templates.list_table.actions.delete"
@icon="far-trash-alt"
@class="btn-danger btn-delete-template"
@action={{this.deleteTemplate}}
/>
</td>
</tr>

View File

@ -1,53 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "I18n";
export default class FormTemplateRowItem extends Component {
@service router;
@service dialog;
@service site;
get activeCategories() {
return this.site?.categories?.filter((c) =>
c["form_template_ids"].includes(this.args.template.id)
);
}
@action
viewTemplate() {
showModal("customize-form-template-view", {
model: this.args.template,
refreshModel: this.args.refreshModel,
});
}
@action
editTemplate() {
this.router.transitionTo(
"adminCustomizeFormTemplates.edit",
this.args.template
);
}
@action
deleteTemplate() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.form_templates.delete_confirm", {
template_name: this.args.template.name,
}),
didConfirm: () => {
ajax(`/admin/customize/form-templates/${this.args.template.id}.json`, {
type: "DELETE",
})
.then(() => {
this.args.refreshModel();
})
.catch(popupAjaxError);
},
});
}
}

View File

@ -1,11 +1,11 @@
import { observes, on } from "@ember-decorators/object"; import { observes, on } from "discourse-common/utils/decorators";
import Component from "@ember/component"; import Component from "@ember/component";
import highlightSyntax from "discourse/lib/highlight-syntax"; import highlightSyntax from "discourse/lib/highlight-syntax";
export default class HighlightedCode extends Component { export default Component.extend({
@on("didInsertElement") @on("didInsertElement")
@observes("code") @observes("code")
_refresh() { _refresh() {
highlightSyntax(this.element, this.siteSettings, this.session); highlightSyntax(this.element, this.siteSettings, this.session);
} },
} });

View File

@ -1,15 +1,15 @@
import { classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@classNames("inline-edit") export default Component.extend({
export default class InlineEditCheckbox extends Component { classNames: ["inline-edit"],
buffer = null;
bufferModelId = null; buffer: null,
bufferModelId: null,
didReceiveAttrs() { didReceiveAttrs() {
super.didReceiveAttrs(...arguments); this._super(...arguments);
if (this.modelId !== this.bufferModelId) { if (this.modelId !== this.bufferModelId) {
// HACK: The condition above ensures this method is called only when its // HACK: The condition above ensures this method is called only when its
@ -24,21 +24,21 @@ export default class InlineEditCheckbox extends Component {
bufferModelId: this.modelId, bufferModelId: this.modelId,
}); });
} }
} },
@discourseComputed("checked", "buffer") @discourseComputed("checked", "buffer")
changed(checked, buffer) { changed(checked, buffer) {
return !!checked !== !!buffer; return !!checked !== !!buffer;
} },
@action @action
apply() { apply() {
this.set("checked", this.buffer); this.set("checked", this.buffer);
this.action(); this.action();
} },
@action @action
cancel() { cancel() {
this.set("buffer", this.checked); this.set("buffer", this.checked);
} },
} });

View File

@ -1,5 +1,4 @@
import { classNames } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@classNames("install-theme-item") classNames: ["install-theme-item"],
export default class InstallThemeItem extends Component {} });

View File

@ -1,5 +1,3 @@
import { classNames } from "@ember-decorators/component";
import { inject as service } from "@ember/service";
import AdminUser from "admin/models/admin-user"; import AdminUser from "admin/models/admin-user";
import Component from "@ember/component"; import Component from "@ember/component";
import EmberObject, { action } from "@ember/object"; import EmberObject, { action } from "@ember/object";
@ -8,11 +6,12 @@ import { ajax } from "discourse/lib/ajax";
import copyText from "discourse/lib/copy-text"; import copyText from "discourse/lib/copy-text";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
@classNames("ip-lookup") export default Component.extend({
export default class IpLookup extends Component { classNames: ["ip-lookup"],
@service dialog; dialog: service(),
@discourseComputed("other_accounts.length", "totalOthersWithSameIP") @discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) { otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
@ -20,100 +19,101 @@ export default class IpLookup extends Component {
const total = Math.min(50, totalOthersWithSameIP || 0); const total = Math.min(50, totalOthersWithSameIP || 0);
const visible = Math.min(50, otherAccountsLength || 0); const visible = Math.min(50, otherAccountsLength || 0);
return Math.max(visible, total); return Math.max(visible, total);
} },
@action @action
hide(event) { hide(event) {
event?.preventDefault(); event?.preventDefault();
this.set("show", false); this.set("show", false);
} },
@action actions: {
lookup() { lookup() {
this.set("show", true); this.set("show", true);
if (!this.location) { if (!this.location) {
ajax("/admin/users/ip-info", { ajax("/admin/users/ip-info", {
data: { ip: this.ip }, data: { ip: this.ip },
}).then((location) => this.set("location", EmberObject.create(location))); }).then((location) =>
} this.set("location", EmberObject.create(location))
);
}
if (!this.other_accounts) { if (!this.other_accounts) {
this.set("otherAccountsLoading", true); this.set("otherAccountsLoading", true);
const data = { const data = {
ip: this.ip, ip: this.ip,
exclude: this.userId, exclude: this.userId,
order: "trust_level DESC", order: "trust_level DESC",
}; };
ajax("/admin/users/total-others-with-same-ip", { ajax("/admin/users/total-others-with-same-ip", {
data, data,
}).then((result) => this.set("totalOthersWithSameIP", result.total)); }).then((result) => this.set("totalOthersWithSameIP", result.total));
AdminUser.findAll("active", data).then((users) => { AdminUser.findAll("active", data).then((users) => {
this.setProperties({ this.setProperties({
other_accounts: users, other_accounts: users,
otherAccountsLoading: false, otherAccountsLoading: 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 = $('<p id="copy-range"></p>');
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
discourseLater(() => this.set("copied", false), 2000);
}
$copyRange.remove();
},
deleteOtherAccounts() {
this.dialog.yesNoConfirm({
message: I18n.t("ip_lookup.confirm_delete_other_accounts"),
didConfirm: () => {
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",
},
})
.catch(popupAjaxError)
.finally(this.send("lookup"));
},
}); });
} },
} },
});
@action
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 = $('<p id="copy-range"></p>');
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
discourseLater(() => this.set("copied", false), 2000);
}
$copyRange.remove();
}
@action
deleteOtherAccounts() {
this.dialog.yesNoConfirm({
message: I18n.t("ip_lookup.confirm_delete_other_accounts"),
didConfirm: () => {
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",
},
})
.catch(popupAjaxError)
.finally(this.send("lookup"));
},
});
}
}

View File

@ -1,5 +1,4 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("tr") tagName: "tr",
export default class ModerationHistoryItem extends Component {} });

View File

@ -1,5 +1,3 @@
import { tagName } from "@ember-decorators/component";
import { inject as service } from "@ember/service";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import Permalink from "admin/models/permalink"; import Permalink from "admin/models/permalink";
@ -7,18 +5,16 @@ import discourseComputed, { bind } from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed"; import { fmt } from "discourse/lib/computed";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { inject as service } from "@ember/service";
@tagName("") export default Component.extend({
export default class PermalinkForm extends Component { tagName: "",
@service dialog; dialog: service(),
formSubmitted: false,
formSubmitted = false; permalinkType: "topic_id",
permalinkType = "topic_id"; permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),
action: null,
@fmt("permalinkType", "admin.permalink.%@") permalinkTypePlaceholder; permalinkTypeValue: null,
action = null;
permalinkTypeValue = null;
@discourseComputed @discourseComputed
permalinkTypes() { permalinkTypes() {
@ -29,21 +25,21 @@ export default class PermalinkForm extends Component {
{ id: "tag_name", name: I18n.t("admin.permalink.tag_name") }, { id: "tag_name", name: I18n.t("admin.permalink.tag_name") },
{ id: "external_url", name: I18n.t("admin.permalink.external_url") }, { id: "external_url", name: I18n.t("admin.permalink.external_url") },
]; ];
} },
@bind @bind
focusPermalink() { focusPermalink() {
schedule("afterRender", () => schedule("afterRender", () =>
document.querySelector(".permalink-url")?.focus() document.querySelector(".permalink-url")?.focus()
); );
} },
@action @action
submitFormOnEnter(event) { submitFormOnEnter(event) {
if (event.key === "Enter") { if (event.key === "Enter") {
this.onSubmit(); this.onSubmit();
} }
} },
@action @action
onSubmit() { onSubmit() {
@ -88,5 +84,5 @@ export default class PermalinkForm extends Component {
} }
); );
} }
} },
} });

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
import FilterComponent from "admin/components/report-filters/filter"; import FilterComponent from "admin/components/report-filters/filter";
export default class List extends FilterComponent {} export default FilterComponent.extend();

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class ResumableUpload extends Component {} export default Component.extend({});

View File

@ -1,23 +0,0 @@
<label>{{i18n "admin.logs.screened_ips.form.label"}}</label>
<TextField
@value={{this.ip_address}}
@disabled={{this.formSubmitted}}
@class="ip-address-input"
@placeholderKey="admin.logs.screened_ips.form.ip_address"
@autocorrect="off"
@autocapitalize="off"
/>
<ComboBox
@content={{this.actionNames}}
@value={{this.actionName}}
@onChange={{action (mut this.actionName)}}
/>
<DButton
@type="submit"
@class="btn-default"
@action={{action "submitForm"}}
@disabled={{this.formSubmitted}}
@label="admin.logs.screened_ips.form.add"
/>

View File

@ -1,11 +1,9 @@
import { action } from "@ember/object";
import { classNames, tagName } from "@ember-decorators/component";
import { inject as service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import ScreenedIpAddress from "admin/models/screened-ip-address"; import ScreenedIpAddress from "admin/models/screened-ip-address";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
/** /**
A form to create an IP address that will be blocked or allowed. A form to create an IP address that will be blocked or allowed.
@ -18,13 +16,12 @@ import { schedule } from "@ember/runloop";
as an argument. as an argument.
**/ **/
@tagName("form") export default Component.extend({
@classNames("screened-ip-address-form", "inline-form") tagName: "form",
export default class ScreenedIpAddressForm extends Component { dialog: service(),
@service dialog; classNames: ["screened-ip-address-form", "inline-form"],
formSubmitted: false,
formSubmitted = false; actionName: "block",
actionName = "block";
@discourseComputed("siteSettings.use_admin_ip_allowlist") @discourseComputed("siteSettings.use_admin_ip_allowlist")
actionNames(adminAllowlistEnabled) { actionNames(adminAllowlistEnabled) {
@ -49,42 +46,43 @@ export default class ScreenedIpAddressForm extends Component {
}, },
]; ];
} }
} },
focusInput() { focusInput() {
schedule("afterRender", () => { schedule("afterRender", () => {
this.element.querySelector("input").focus(); this.element.querySelector("input").focus();
}); });
} },
@action actions: {
submitForm() { submit() {
if (!this.formSubmitted) { if (!this.formSubmitted) {
this.set("formSubmitted", true); this.set("formSubmitted", true);
const screenedIpAddress = ScreenedIpAddress.create({ const screenedIpAddress = ScreenedIpAddress.create({
ip_address: this.ip_address, ip_address: this.ip_address,
action_name: this.actionName, action_name: this.actionName,
});
screenedIpAddress
.save()
.then((result) => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
this.focusInput();
})
.catch((e) => {
this.set("formSubmitted", false);
const message = e.jqXHR.responseJSON?.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
this.dialog.alert({
message,
didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(),
});
}); });
} screenedIpAddress
} .save()
} .then((result) => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
this.focusInput();
})
.catch((e) => {
this.set("formSubmitted", false);
const message = e.jqXHR.responseJSON?.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
this.dialog.alert({
message,
didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(),
});
});
}
},
},
});

View File

@ -1,16 +1,15 @@
import { classNameBindings } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
import I18n from "I18n"; import I18n from "I18n";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { on } from "@ember-decorators/object"; import { on } from "discourse-common/utils/decorators";
import { action, set } from "@ember/object"; import { set } from "@ember/object";
@classNameBindings(":value-list", ":secret-value-list") export default Component.extend({
export default class SecretValueList extends Component { classNameBindings: [":value-list", ":secret-value-list"],
inputDelimiter = null; inputDelimiter: null,
collection = null; collection: null,
values = null; values: null,
validationMessage = null; validationMessage: null,
@on("didReceiveAttrs") @on("didReceiveAttrs")
_setupCollection() { _setupCollection() {
@ -20,43 +19,41 @@ export default class SecretValueList extends Component {
"collection", "collection",
this._splitValues(values, this.inputDelimiter || "\n") this._splitValues(values, this.inputDelimiter || "\n")
); );
} },
@action actions: {
changeKey(index, event) { changeKey(index, event) {
const newValue = event.target.value; const newValue = event.target.value;
if (this._checkInvalidInput(newValue)) { if (this._checkInvalidInput(newValue)) {
return; return;
} }
this._replaceValue(index, newValue, "key"); this._replaceValue(index, newValue, "key");
} },
@action changeSecret(index, event) {
changeSecret(index, event) { const newValue = event.target.value;
const newValue = event.target.value;
if (this._checkInvalidInput(newValue)) { if (this._checkInvalidInput(newValue)) {
return; return;
} }
this._replaceValue(index, newValue, "secret"); this._replaceValue(index, newValue, "secret");
} },
@action addValue() {
addValue() { if (this._checkInvalidInput([this.newKey, this.newSecret])) {
if (this._checkInvalidInput([this.newKey, this.newSecret])) { return;
return; }
} this._addValue(this.newKey, this.newSecret);
this._addValue(this.newKey, this.newSecret); this.setProperties({ newKey: "", newSecret: "" });
this.setProperties({ newKey: "", newSecret: "" }); },
}
@action removeValue(value) {
removeValue(value) { this._removeValue(value);
this._removeValue(value); },
} },
_checkInvalidInput(inputs) { _checkInvalidInput(inputs) {
this.set("validationMessage", null); this.set("validationMessage", null);
@ -69,25 +66,25 @@ export default class SecretValueList extends Component {
return true; return true;
} }
} }
} },
_addValue(value, secret) { _addValue(value, secret) {
this.collection.addObject({ key: value, secret }); this.collection.addObject({ key: value, secret });
this._saveValues(); this._saveValues();
} },
_removeValue(value) { _removeValue(value) {
const collection = this.collection; const collection = this.collection;
collection.removeObject(value); collection.removeObject(value);
this._saveValues(); this._saveValues();
} },
_replaceValue(index, newValue, keyName) { _replaceValue(index, newValue, keyName) {
let item = this.collection[index]; let item = this.collection[index];
set(item, keyName, newValue); set(item, keyName, newValue);
this._saveValues(); this._saveValues();
} },
_saveValues() { _saveValues() {
this.set( this.set(
@ -98,7 +95,7 @@ export default class SecretValueList extends Component {
}) })
.join("\n") .join("\n")
); );
} },
_splitValues(values, delimiter) { _splitValues(values, delimiter) {
if (values && values.length) { if (values && values.length) {
@ -116,5 +113,5 @@ export default class SecretValueList extends Component {
} else { } else {
return []; return [];
} }
} },
} });

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class SettingValidationMessage extends Component {} export default Component.extend({});

View File

@ -1,5 +1,4 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({
@tagName("") tagName: "",
export default class SilenceDetails extends Component {} });

View File

@ -1,37 +1,34 @@
import { classNameBindings } from "@ember-decorators/component";
import { empty } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators"; import { empty } from "@ember/object/computed";
import { on } from "@ember-decorators/object"; import discourseComputed, { on } from "discourse-common/utils/decorators";
@classNameBindings(":simple-list", ":value-list") export default Component.extend({
export default class SimpleList extends Component { classNameBindings: [":simple-list", ":value-list"],
@empty("newValue") inputEmpty; inputEmpty: empty("newValue"),
inputDelimiter: null,
inputDelimiter = null; newValue: "",
newValue = ""; collection: null,
collection = null; values: null,
values = null;
@on("didReceiveAttrs") @on("didReceiveAttrs")
_setupCollection() { _setupCollection() {
this.set("collection", this._splitValues(this.values, this.inputDelimiter)); this.set("collection", this._splitValues(this.values, this.inputDelimiter));
} },
keyDown(event) { keyDown(event) {
if (event.which === 13) { if (event.which === 13) {
this.addValue(this.newValue); this.addValue(this.newValue);
return; return;
} }
} },
@action @action
changeValue(index, event) { changeValue(index, event) {
this.collection.replace(index, 1, [event.target.value]); this.collection.replace(index, 1, [event.target.value]);
this.collection.arrayContentDidChange(index); this.collection.arrayContentDidChange(index);
this._onChange(); this._onChange();
} },
@action @action
addValue(newValue) { addValue(newValue) {
@ -42,13 +39,13 @@ export default class SimpleList extends Component {
this.set("newValue", null); this.set("newValue", null);
this.collection.addObject(newValue); this.collection.addObject(newValue);
this._onChange(); this._onChange();
} },
@action @action
removeValue(value) { removeValue(value) {
this.collection.removeObject(value); this.collection.removeObject(value);
this._onChange(); this._onChange();
} },
@action @action
shift(operation, index) { shift(operation, index) {
@ -65,20 +62,20 @@ export default class SimpleList extends Component {
this.collection.insertAt(futureIndex, shiftedValue); this.collection.insertAt(futureIndex, shiftedValue);
this._onChange(); this._onChange();
} },
_onChange() { _onChange() {
this.onChange?.(this.collection); this.onChange?.(this.collection);
} },
@discourseComputed("collection") @discourseComputed("collection")
showUpDownButtons(collection) { showUpDownButtons(collection) {
return collection.length - 1 ? true : false; return collection.length - 1 ? true : false;
} },
_splitValues(values, delimiter) { _splitValues(values, delimiter) {
return values && values.length return values && values.length
? values.split(delimiter || "\n").filter(Boolean) ? values.split(delimiter || "\n").filter(Boolean)
: []; : [];
} },
} });

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class SiteCustomizationChangeDetails extends Component {} export default Component.extend({});

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class SiteCustomizationChangeField extends Component {} export default Component.extend({});

View File

@ -1,55 +0,0 @@
<div class="setting-label">
<h3>
{{this.settingName}}
{{#if this.staffLogFilter}}
<LinkTo
@route="adminLogs.staffActionLogs"
@query={{hash filters=this.staffLogFilter force_refresh=true}}
title={{i18n "admin.settings.history"}}
>
<span class="history-icon">
{{d-icon "history"}}
</span>
</LinkTo>
{{/if}}
</h3>
{{#if this.defaultIsAvailable}}
<DButton
class="btn-link"
@action={{this.setDefaultValues}}
@translatedLabel={{this.setting.setDefaultValuesLabel}}
/>
{{/if}}
</div>
<div class="setting-value">
{{component
this.componentName
setting=this.setting
value=this.buffered.value
validationMessage=this.validationMessage
preview=this.preview
isSecret=this.isSecret
allowAny=this.allowAny
}}
</div>
{{#if this.dirty}}
<div class="setting-controls">
<DButton class="ok" @action={{this.update}} @icon="check" />
<DButton class="cancel" @action={{this.cancel}} @icon="times" />
</div>
{{else if this.setting.overridden}}
{{#if this.setting.secret}}
<DButton @action={{this.toggleSecret}} @icon="far-eye-slash" />
{{/if}}
<DButton
class="btn-default undo"
@action={{this.resetDefault}}
@icon="undo"
@label="admin.settings.reset"
/>
{{/if}}

View File

@ -1,21 +1,18 @@
import { readOnly } from "@ember/object/computed";
import BufferedContent from "discourse/mixins/buffered-content"; import BufferedContent from "discourse/mixins/buffered-content";
import Component from "@ember/component"; import Component from "@ember/component";
import SettingComponent from "admin/mixins/setting-component"; import SettingComponent from "admin/mixins/setting-component";
import SiteSetting from "admin/models/site-setting"; import SiteSetting from "admin/models/site-setting";
import { readOnly } from "@ember/object/computed";
export default class SiteSettingComponent extends Component.extend( export default Component.extend(BufferedContent, SettingComponent, {
BufferedContent, updateExistingUsers: null,
SettingComponent
) {
updateExistingUsers = null;
@readOnly("setting.staffLogFilter") staffLogFilter;
_save() { _save() {
const setting = this.buffered; const setting = this.buffered;
return SiteSetting.update(setting.get("setting"), setting.get("value"), { return SiteSetting.update(setting.get("setting"), setting.get("value"), {
updateExistingUsers: this.updateExistingUsers, updateExistingUsers: this.updateExistingUsers,
}); });
} },
}
staffLogFilter: readOnly("setting.staffLogFilter"),
});

View File

@ -1,18 +1,19 @@
import { computed } from "@ember/object";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
export default class Bool extends Component { export default Component.extend({
@computed("value") @discourseComputed("value")
get enabled() { enabled: {
if (isEmpty(this.value)) { get(value) {
return false; if (isEmpty(value)) {
} return false;
return this.value.toString() === "true"; }
} return value.toString() === "true";
},
set enabled(value) { set(value) {
this.set("value", value ? "true" : "false"); this.set("value", value ? "true" : "false");
return value; return value;
} },
} },
});

View File

@ -1,15 +1,15 @@
import { action, computed } from "@ember/object";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import Component from "@ember/component"; import Component from "@ember/component";
import { computed } from "@ember/object";
export default class CategoryList extends Component { export default Component.extend({
@computed("value") selectedCategories: computed("value", function () {
get selectedCategories() {
return Category.findByIds(this.value.split("|").filter(Boolean)); return Category.findByIds(this.value.split("|").filter(Boolean));
} }),
@action actions: {
onChangeSelectedCategories(value) { onChangeSelectedCategories(value) {
this.set("value", (value || []).mapBy("id").join("|")); this.set("value", (value || []).mapBy("id").join("|"));
} },
} },
});

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class Category extends Component {} export default Component.extend({});

View File

@ -24,9 +24,8 @@ function RGBToHex(rgb) {
return "#" + r + g + b; return "#" + r + g + b;
} }
export default class Color extends Component { export default Component.extend({
@computed("value") valid: computed("value", function () {
get valid() {
let value = this.value.toLowerCase(); let value = this.value.toLowerCase();
let testColor = new Option().style; let testColor = new Option().style;
@ -44,10 +43,10 @@ export default class Color extends Component {
} }
return testColor.color && hexifiedColor === value; return testColor.color && hexifiedColor === value;
} }),
@action @action
onChangeColor(color) { onChangeColor(color) {
this.set("value", color); this.set("value", color);
} },
} });

View File

@ -1,36 +1,40 @@
import { action, computed } from "@ember/object";
import Component from "@ember/component"; import Component from "@ember/component";
import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers"; import { makeArray } from "discourse-common/lib/helpers";
export default class CompactList extends Component { export default Component.extend({
tokenSeparator = "|"; tokenSeparator: "|",
createdChoices = null;
@computed("value") createdChoices: null,
get settingValue() {
settingValue: computed("value", function () {
return this.value.toString().split(this.tokenSeparator).filter(Boolean); return this.value.toString().split(this.tokenSeparator).filter(Boolean);
} }),
@computed("settingValue", "setting.choices.[]", "createdChoices.[]") settingChoices: computed(
get settingChoices() { "settingValue",
return [ "setting.choices.[]",
...new Set([ "createdChoices.[]",
...makeArray(this.settingValue), function () {
...makeArray(this.setting.choices), return [
...makeArray(this.createdChoices), ...new Set([
]), ...makeArray(this.settingValue),
]; ...makeArray(this.setting.choices),
} ...makeArray(this.createdChoices),
]),
];
}
),
@action actions: {
onChangeListSetting(value) { onChangeListSetting(value) {
this.set("value", value.join(this.tokenSeparator)); this.set("value", value.join(this.tokenSeparator));
} },
@action onChangeChoices(choices) {
onChangeChoices(choices) { this.set("createdChoices", [
this.set("createdChoices", [ ...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]),
...new Set([...makeArray(this.createdChoices), ...makeArray(choices)]), ]);
]); },
} },
} });

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class EmojiList extends Component {} export default Component.extend({});

View File

@ -1,3 +1,3 @@
import Component from "@ember/component"; import Component from "@ember/component";
export default class Enum extends Component {} export default Component.extend({});

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