Compare commits

..

28 Commits

Author SHA1 Message Date
Martin Brennan
6614f789fa
more debugging 2022-12-14 18:48:34 +10:00
Martin Brennan
1aa304850e
DEV: Put reply control HTML 2022-12-14 18:27:20 +10:00
Martin Brennan
ed30b241f9
change set to fill_in and remove within from send_reply 2022-12-14 17:44:04 +10:00
Martin Brennan
006c47cbd1
change set to fill_in 2022-12-14 17:20:45 +10:00
Martin Brennan
c7a3fcd33a
add sleep 2022-12-14 17:03:10 +10:00
Martin Brennan
2454a251cd
use reply_control in one test 2022-12-14 16:43:51 +10:00
Martin Brennan
eb5a39a98c
DEV: Remove unused fabricators 2022-12-14 15:19:34 +10:00
Martin Brennan
4d587da197
DEV: Minor spec fixes 2022-12-14 10:53:54 +10:00
Joffrey JAFFEUX
c666b8154c will work 2022-12-13 21:50:49 +01:00
Joffrey JAFFEUX
f5ca3a9b14 test 2022-12-13 21:22:27 +01:00
Joffrey JAFFEUX
f43dd753ba is that it? 2022-12-13 21:03:12 +01:00
Joffrey JAFFEUX
53d605c238 debug 2022-12-13 20:51:25 +01:00
Joffrey JAFFEUX
bed2b26c18 disable animations 2022-12-13 20:19:10 +01:00
Joffrey JAFFEUX
cbe0886836 debug 2022-12-13 18:43:31 +01:00
Joffrey JAFFEUX
11ec0ea8a3 debugging 2022-12-13 18:42:18 +01:00
Joffrey JAFFEUX
949d2f95fc more debugging 2022-12-13 18:13:46 +01:00
Joffrey JAFFEUX
99a21cd633 debug 2022-12-13 18:02:01 +01:00
Joffrey JAFFEUX
f0d8663202 adds wait 2022-12-13 17:27:19 +01:00
Joffrey JAFFEUX
252fef0e70 uses selector 2022-12-13 16:59:30 +01:00
Joffrey JAFFEUX
1f4393fd1b better 2022-12-13 15:57:11 +01:00
Joffrey JAFFEUX
c6aa8c1da2 fix tests. 2022-12-13 15:39:02 +01:00
Joffrey JAFFEUX
f6c35239dd fix tests? 2022-12-13 14:10:22 +01:00
Joffrey JAFFEUX
291701034c fix tests 2022-12-13 13:51:17 +01:00
Joffrey JAFFEUX
8e91c9cc92 bootstrap with a different user 2022-12-13 11:56:21 +01:00
Joffrey JAFFEUX
792fcb2e01 ensures composer is expanded 2022-12-13 10:38:02 +01:00
Martin Brennan
51c3166c67
Merge branch 'main' into dev/chat-transcript-system-spec 2022-12-13 18:20:29 +10:00
Martin Brennan
aa9b945be1
Merge branch 'main' into dev/chat-transcript-system-spec 2022-12-13 16:12:05 +10:00
Martin Brennan
e4fe322fa4
DEV: Move chat transcript tests into system specs
We are all in on system specs, so this commit moves all
the chat quoting acceptance tests (some of which have been
skipped for a while) into system specs.
2022-12-13 15:42:11 +10:00
6098 changed files with 164022 additions and 237872 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

@ -15,7 +15,6 @@ permissions:
jobs: jobs:
build: build:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
name: run name: run
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: discourse/discourse_test:slim container: discourse/discourse_test:slim

View File

@ -15,16 +15,12 @@ permissions:
jobs: jobs:
build: build:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
name: run name: run
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: discourse/discourse_test:slim container: discourse/discourse_test:slim
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 +67,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 +82,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

@ -17,10 +17,9 @@ permissions:
jobs: jobs:
build: build:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')" name: ${{ matrix.target }} ${{ matrix.build_type }}
name: ${{ matrix.target }} ${{ matrix.build_type }} ${{ matrix.ruby }}
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 +28,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 +36,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 +43,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 +66,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 +153,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 +170,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
@ -229,7 +203,6 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
core_frontend_tests: core_frontend_tests:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
name: core frontend (${{ matrix.browser }}) name: core frontend (${{ matrix.browser }})
runs-on: ubuntu-20.04-8core runs-on: ubuntu-20.04-8core
container: container:

3
.gitignore vendored
View File

@ -50,9 +50,6 @@
/vendor/data/GeoLite2-City.mmdb /vendor/data/GeoLite2-City.mmdb
/vendor/data/GeoLite2-ASN.mmdb /vendor/data/GeoLite2-ASN.mmdb
# We provide a .sample but people can use newer versions if they want to
.ruby-version
# Front-end # Front-end
dist dist
node_modules node_modules

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

@ -1,6 +1,5 @@
sources: sources:
bundler: true bundler: true
allowed: allowed:
- 0bsd - 0bsd
- apache-2.0 - apache-2.0
@ -13,20 +12,8 @@ allowed:
ignored: ignored:
bundler: bundler:
- cgi # Ruby (default gem) - rchardet # Ruby terms
- date # Ruby (default gem) - strscan # Ruby
- digest # Ruby (default gem)
- io-wait # Ruby (default gem)
- json # Ruby (default gem)
- net-http # Ruby (default gem)
- net-protocol # Ruby (default gem)
- openssl # Ruby (default gem)
- racc # Ruby (default gem)
- rchardet # LGPL
- ruby2_keywords # Ruby (default gem)
- strscan # Ruby (default gem)
- timeout # Ruby (default gem)
- uri # Ruby (default gem)
reviewed: reviewed:
bundler: bundler:
@ -39,24 +26,32 @@ reviewed:
- faraday-em_synchrony # MIT - faraday-em_synchrony # MIT
- faraday-excon # MIT - faraday-excon # MIT
- faraday-httpclient # MIT - faraday-httpclient # MIT
- faraday-net_http # MIT
- faraday-patron # MIT - faraday-patron # MIT
- faraday-net_http # MIT
- faraday-rack # MIT - faraday-rack # MIT
- highline # Ruby or GPL-2.0 - highline # GPL-2.0 OR Ruby terms
- htmlentities # MIT - htmlentities # MIT
- image_size # MIT - image_size # MIT
- io-wait # Ruby terms
- json # Ruby terms
- jwt # MIT - jwt # MIT
- kgio # LGPL-2.1+ - kgio # LGPL-2.1+
- logstash-event # Apache-2.0 - logstash-event # Apache-2.0
- net-imap # Ruby (bundled gem) - net-http # Ruby
- net-pop # Ruby (bundled gem) - net-imap # Ruby
- net-smtp # Ruby (bundled gem) - net-pop # Ruby
- net-protocol # Ruby
- net-smtp # Ruby
- omniauth # MIT - omniauth # MIT
- pg # Ruby - openssl # Ruby terms
- pg # Ruby terms
- r2 # Apache-2.0 (Twitter) - r2 # Apache-2.0 (Twitter)
- racc # Ruby terms
- raindrops # LGPL-2.1+ - raindrops # LGPL-2.1+
- rubyzip # Ruby - rubyzip # Ruby terms
- sidekiq # LGPL (Sidekiq) - sidekiq # LGPL (Sidekiq)
- tilt # MIT - tilt
- timeout # Ruby
- unf # BSD-2-Clause - unf # BSD-2-Clause
- unicorn # Ruby or GPLv2/GPLv3 - unicorn
- uri # Ruby

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
@ -27,6 +26,7 @@ dist/
tmp/ tmp/
**/*.rb **/*.rb
**/*.hbs
**/*.html **/*.html
**/*.json **/*.json
**/*.md **/*.md

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 2.7.6

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,17 +15,11 @@ 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": {
allow: ["loading-spinner"], allow: ["loading-spinner"],
}, },
// Begin prettier compatibility
"eol-last": false,
"self-closing-void-elements": false,
"block-indentation": false,
quotes: false,
// End prettier compatibility
}, },
}; };

299
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,138 @@ 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" gem 'webpush', require: false
gem "colored2", require: false gem 'colored2', require: false
gem "maxminddb" 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
gem "webrick", require: false
# Workaround until Ruby ships with cgi version 0.3.6 or higher.
gem "cgi", ">= 0.3.6", require: false
gem "tzinfo-data"

View File

@ -5,37 +5,28 @@ GIT
mail (2.8.0.edge) mail (2.8.0.edge)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
GIT
remote: https://github.com/rails/sprockets
revision: f4d3dae71ef29c44b75a49cfbf8032cce07b423a
branch: 3.x
specs:
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
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 +35,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 +79,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.4)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
byebug (11.1.3) byebug (11.1.3)
@ -106,11 +97,10 @@ GEM
xpath (~> 3.2) xpath (~> 3.2)
cbor (0.5.9.6) cbor (0.5.9.6)
certified (1.0.0) certified (1.0.0)
cgi (0.3.6)
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 +109,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.12.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 +126,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.1)
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,24 +146,19 @@ 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)
hashie (5.0.0) hashie (5.0.0)
highline (2.1.0) highline (2.0.3)
hkdf (1.0.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
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 +175,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.5.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 +183,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,8 +195,8 @@ 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.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lru_redux (1.1.0) lru_redux (1.1.0)
@ -228,11 +204,11 @@ 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)
mini_portile2 (2.8.1) mini_portile2 (2.8.0)
mini_racer (0.6.3) mini_racer (0.6.3)
libv8-node (~> 16.10.0.0) libv8-node (~> 16.10.0.0)
mini_scheduler (0.15.0) mini_scheduler (0.15.0)
@ -240,17 +216,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.16.3)
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.2.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 +234,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 +280,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.1)
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,21 +300,22 @@ 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.0)
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.4)
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.4)
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.3)
loofah (~> 2.19, >= 2.19.1) loofah (~> 2.3)
rails_failover (0.8.1) rails_failover (0.8.1)
activerecord (> 6.0, < 7.1) activerecord (> 6.0, < 7.1)
concurrent-ruby concurrent-ruby
@ -348,15 +323,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,15 +341,15 @@ 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)
rinku (2.0.6) rinku (2.0.6)
rotp (6.2.2) rotp (6.2.1)
rqrcode (2.1.2) rqrcode (2.1.2)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
@ -383,15 +358,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.0)
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,60 +385,53 @@ 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.40.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.23.0, < 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.0)
parser (>= 3.2.1.0) parser (>= 3.1.1.0)
rubocop-capybara (2.17.1) rubocop-discourse (3.0)
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.15.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara (~> 2.17) ruby-prof (1.4.3)
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)
shoulda-matchers (5.3.0) shoulda-matchers (5.2.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (6.5.8) sidekiq (6.5.8)
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 +440,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 +468,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)
@ -514,15 +476,15 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.7.0) webpush (1.1.0)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket (1.2.9) websocket (1.2.9)
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 +496,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
@ -554,13 +516,10 @@ DEPENDENCIES
capybara capybara
cbor cbor
certified certified
cgi (>= 0.3.6)
colored2 colored2
cose cose
cppjieba_rb cppjieba_rb
css_parser css_parser
dartsass-ruby
dartsass-sprockets
diffy diffy
digest digest
discourse-fonts discourse-fonts
@ -622,12 +581,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 +602,31 @@ 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
webdrivers webdrivers
webmock webmock
webrick webpush
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 +0,0 @@
<div class="ace">{{this.content}}</div>

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,8 +0,0 @@
{{#if this.hasFormattedLogs}}
<pre>{{this.formattedLogs}}</pre>
{{else}}
<p>{{this.noLogsMessage}}</p>
{{/if}}
{{#if this.showLoadingSpinner}}
<div class="spinner small"></div>
{{/if}}

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,30 +0,0 @@
<div class="field">{{i18n this.name}}</div>
<div class="value">
{{#if this.editing}}
<TextField
@value={{this.buffer}}
@autofocus="autofocus"
@autocomplete="off"
/>
{{else}}
<a href {{on "click" this.edit}} class="inline-editable-field">
<span>{{this.value}}</span>
</a>
{{/if}}
</div>
<div class="controls">
{{#if this.editing}}
<DButton
@class="btn-default"
@action={{action "save"}}
@label="admin.user_fields.save"
/>
<a href {{on "click" this.edit}}>{{i18n "cancel"}}</a>
{{else}}
<DButton
@class="btn-default"
@action={{action "edit"}}
@icon="pencil-alt"
/>
{{/if}}
</div>

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,14 +0,0 @@
<div class="form-element label-area">
{{#if this.label}}
<label>{{i18n this.label}}</label>
{{else}}
&nbsp;
{{/if}}
</div>
<div class="form-element input-area">
{{#if this.wrapLabel}}
<label>{{yield}}</label>
{{else}}
{{yield}}
{{/if}}
</div>

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,7 +0,0 @@
<div class="admin-controls">
<nav>
<ul class="nav nav-pills">
{{yield}}
</ul>
</nav>
</div>

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,14 +0,0 @@
<div
class="suspended-count {{this.suspendedCountClass}}"
title={{i18n "admin.user.last_six_months"}}
>
<label>{{i18n "admin.user.suspended_count"}}</label>
<span>{{this.user.penalty_counts.suspended}}</span>
</div>
<div
class="silenced-count {{this.silencedCountClass}}"
title={{i18n "admin.user.last_six_months"}}
>
<label>{{i18n "admin.user.silenced_count"}}</label>
<span>{{this.user.penalty_counts.silenced}}</span>
</div>

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,18 +0,0 @@
<div class="penalty-post-controls">
<label>
<div class="penalty-post-label">
{{html-safe (i18n "admin.user.penalty_post_actions")}}
</div>
</label>
<ComboBox
@value={{this.postAction}}
@content={{this.penaltyActions}}
@onChange={{action "penaltyChanged"}}
/>
</div>
{{#if this.editing}}
<div class="penalty-post-edit">
<Textarea @value={{this.postEdit}} class="post-editor" />
</div>
{{/if}}

View File

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

View File

@ -1,40 +0,0 @@
<div class="penalty-reason-controls">
{{#if (eq @penaltyType "suspend")}}
<label class="suspend-reason-title">{{i18n
"admin.user.suspend_reason_title"
}}</label>
<ComboBox
@content={{this.reasons}}
@value={{this.selectedReason}}
@class="suspend-reason"
@onChange={{this.setSelectedReason}}
/>
{{#if this.isCustomReason}}
<TextField
@value={{this.customReason}}
@class="suspend-reason"
@onChange={{this.setCustomReason}}
/>
{{/if}}
{{else if (eq @penaltyType "silence")}}
<label class="silence-reason-title">{{html-safe
(i18n "admin.user.silence_reason_label")
}}</label>
<TextField
@value={{this.customReason}}
@class="silence-reason"
@onChange={{this.setCustomReason}}
@placeholderKey="admin.user.silence_reason_placeholder"
/>
{{/if}}
</div>
<div class="penalty-message-controls">
<label>{{i18n "admin.user.suspend_message"}}</label>
<Textarea
@value={{this.message}}
class="suspend-message"
placeholder={{i18n "admin.user.suspend_message_placeholder"}}
/>
</div>

View File

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

View File

@ -1,44 +0,0 @@
<div class="penalty-similar-users">
<p class="alert alert-warning">
{{html-safe
(i18n
"admin.user.other_matches"
(hash count=this.user.similar_users_count username=this.user.username)
)
}}
</p>
<table class="table">
<thead>
<tr>
<th></th>
<th>{{i18n "username"}}</th>
<th>{{i18n "last_seen"}}</th>
<th>{{i18n "admin.user.topics_entered"}}</th>
<th>{{i18n "admin.user.posts_read_count"}}</th>
<th>{{i18n "admin.user.time_read"}}</th>
<th>{{i18n "created"}}</th>
</tr>
</thead>
<tbody>
{{#each this.user.similar_users as |user|}}
<tr>
<td>
<Input
@type="checkbox"
disabled={{not (get user this.penaltyField)}}
{{on "click" (action "selectUserId" user.id)}}
/>
</td>
<td>{{avatar user imageSize="small"}} {{user.username}}</td>
<td>{{format-duration user.last_seen_age}}</td>
<td>{{number user.topics_entered}}</td>
<td>{{number user.posts_read_count}}</td>
<td>{{format-duration user.time_read}}</td>
<td>{{format-duration user.created_at_age}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>

View File

@ -1,18 +1,18 @@
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("type")
penaltyField(penaltyType) { penaltyField(penaltyType) {
if (penaltyType === "suspend") { if (penaltyType === "suspend") {
return "can_be_suspended"; return "can_be_suspended";
} 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,3 +0,0 @@
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>
</div>

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,35 +0,0 @@
<div class="cell title">
{{#if this.model.icon}}
{{d-icon this.model.icon}}
{{/if}}
<a href={{this.model.reportUrl}}>{{this.model.title}}</a>
</div>
<div class="cell value today-count">{{number this.model.todayCount}}</div>
<div
class="cell value yesterday-count {{this.model.yesterdayTrend}}"
title={{this.model.yesterdayCountTitle}}
>
{{number this.model.yesterdayCount}}
{{d-icon this.model.yesterdayTrendIcon}}
</div>
<div
class="cell value sevendays-count {{this.model.sevenDaysTrend}}"
title={{this.model.sevenDaysCountTitle}}
>
{{number this.model.lastSevenDaysCount}}
{{d-icon this.model.sevenDaysTrendIcon}}
</div>
<div
class="cell value thirty-days-count {{this.model.thirtyDaysTrend}}"
title={{this.model.thirtyDaysCountTitle}}
>
{{number this.model.lastThirtyDaysCount}}
{{#if this.model.canDisplayTrendIcon}}
{{d-icon this.model.thirtyDaysTrendIcon}}
{{/if}}
</div>

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,36 +0,0 @@
<td class="title">
{{#if this.report.icon}}
{{d-icon this.report.icon}}
{{/if}}
<a href={{this.report.reportUrl}}>{{this.report.title}}</a>
</td>
<td class="value">{{number this.report.todayCount}}</td>
<td
class="value {{this.report.yesterdayTrend}}"
title={{this.report.yesterdayCountTitle}}
>
{{number this.report.yesterdayCount}}
{{d-icon this.report.yesterdayTrendIcon}}
</td>
<td
class="value {{this.report.sevenDaysTrend}}"
title={{this.report.sevenDaysCountTitle}}
>
{{number this.report.lastSevenDaysCount}}
{{d-icon this.report.sevenDaysTrendIcon}}
</td>
<td
class="value {{this.report.thirtyDaysTrend}}"
title={{this.report.thirtyDaysCountTitle}}
>
{{number this.report.lastThirtyDaysCount}}
{{d-icon this.report.thirtyDaysTrendIcon}}
</td>
{{#if this.allTime}}
<td class="value">{{number this.report.total}}</td>
{{/if}}

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,15 +0,0 @@
<div class="table-container">
{{#each this.model.data as |data|}}
<a class="table-cell user-{{data.key}}" href={{data.url}}>
<span class="label">
{{#if data.icon}}
{{d-icon data.icon}}
{{/if}}
{{data.x}}
</span>
<span class="value">
{{number data.y}}
</span>
</a>
{{/each}}
</div>

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,8 +0,0 @@
<td class="title"><a
href={{this.report.reportUrl}}
>{{this.report.title}}</a></td>
<td class="value">{{this.report.todayCount}}</td>
<td class="value">{{this.report.yesterdayCount}}</td>
<td class="value">{{this.report.sevenDaysAgoCount}}</td>
<td class="value">{{this.report.thirtyDaysAgoCount}}</td>
<td class="value"></td>

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,3 +0,0 @@
<div class="chart-canvas-container">
<canvas class="chart-canvas"></canvas>
</div>

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,54 +0,0 @@
{{#if this.showBackupStats}}
<div class="backups">
<h3 class="storage-stats-title">
<a href={{get-url "/admin/backups"}}>{{d-icon "archive"}}
{{i18n "admin.dashboard.backups"}}</a>
</h3>
<p>
{{#if this.backupStats.free_bytes}}
{{i18n
"admin.dashboard.space_used_and_free"
usedSize=this.usedBackupSpace
freeSize=this.freeBackupSpace
}}
{{else}}
{{i18n "admin.dashboard.space_used" usedSize=this.usedBackupSpace}}
{{/if}}
<br />
{{i18n
"admin.dashboard.backup_count"
count=this.backupStats.count
location=this.backupLocationName
}}
{{#if this.backupStats.last_backup_taken_at}}
<br />
{{html-safe
(i18n
"admin.dashboard.lastest_backup"
date=(format-date
this.backupStats.last_backup_taken_at leaveAgo="true"
)
)
}}
{{/if}}
</p>
</div>
{{/if}}
<div class="uploads">
<h3 class="storage-stats-title">{{d-icon "upload"}}
{{i18n "admin.dashboard.uploads"}}</h3>
<p>
{{#if this.uploadStats.free_bytes}}
{{i18n
"admin.dashboard.space_used_and_free"
usedSize=this.usedUploadSpace
freeSize=this.freeUploadSpace
}}
{{else}}
{{i18n "admin.dashboard.space_used" usedSize=this.usedUploadSpace}}
{{/if}}
</p>
</div>

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 +0,0 @@
{{html-safe this.formattedValue}}

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,13 +0,0 @@
{{#if this.showSortingUI}}
<DButton
@action={{this.sortByLabel}}
@icon={{this.sortIcon}}
@class="sort-btn"
/>
{{/if}}
{{#if this.label.htmlTitle}}
<span class="title">{{html-safe this.label.htmlTitle}}</span>
{{else}}
<span class="title">{{this.label.title}}</span>
{{/if}}

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,7 +0,0 @@
{{#each this.labels as |label|}}
<AdminReportTableCell
@label={{label}}
@data={{this.data}}
@options={{this.options}}
/>
{{/each}}

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,84 +0,0 @@
<table class="table">
<thead>
<tr>
{{#if this.model.computedLabels}}
{{#each this.model.computedLabels as |label|}}
<AdminReportTableHeader
@showSortingUI={{this.showSortingUI}}
@currentSortDirection={{this.sortDirection}}
@currentSortLabel={{this.sortLabel}}
@label={{label}}
@sortByLabel={{action "sortByLabel" label}}
/>
{{/each}}
{{else}}
{{#each this.model.data as |data|}}
<th>{{data.x}}</th>
{{/each}}
{{/if}}
</tr>
</thead>
<tbody>
{{#each this.paginatedData as |data|}}
<AdminReportTableRow
@data={{data}}
@labels={{this.model.computedLabels}}
@options={{this.options}}
/>
{{/each}}
{{#if this.showTotalForSample}}
<tr class="total-row">
<td colspan={{this.totalsForSample.length}}>
{{i18n "admin.dashboard.reports.totals_for_sample"}}
</td>
</tr>
<tr class="admin-report-table-row">
{{#each this.totalsForSample as |total|}}
<td class="admin-report-table-cell {{total.type}} {{total.property}}">
{{total.formattedValue}}
</td>
{{/each}}
</tr>
{{/if}}
{{#if this.showTotal}}
<tr class="total-row">
<td colspan="2">
{{i18n "admin.dashboard.reports.total"}}
</td>
</tr>
<tr class="admin-report-table-row">
<td class="admin-report-table-cell date x">—</td>
<td class="admin-report-table-cell number y">{{number
this.model.total
}}</td>
</tr>
{{/if}}
{{#if this.showAverage}}
<tr class="total-row">
<td colspan="2">
{{i18n "admin.dashboard.reports.average_for_sample"}}
</td>
</tr>
<tr class="admin-report-table-row">
<td class="admin-report-table-cell date x">—</td>
<td class="admin-report-table-cell number y">{{number
this.averageForSample
}}</td>
</tr>
{{/if}}
</tbody>
</table>
<div class="pagination">
{{#each this.pages as |pageState|}}
<DButton
@translatedLabel={{pageState.page}}
@action={{action "changePage"}}
@actionParam={{pageState.index}}
@class={{pageState.class}}
/>
{{/each}}
</div>

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,26 +0,0 @@
<td class="title">{{this.report.title}}</td>
<td class="value">
<LinkTo @route="adminUsersList.show" @model="newuser">
{{number (value-at-tl this.report.data level="0")}}
</LinkTo>
</td>
<td class="value">
<LinkTo @route="adminUsersList.show" @model="basic">
{{number (value-at-tl this.report.data level="1")}}
</LinkTo>
</td>
<td class="value">
<LinkTo @route="adminUsersList.show" @model="member">
{{number (value-at-tl this.report.data level="2")}}
</LinkTo>
</td>
<td class="value">
<LinkTo @route="adminUsersList.show" @model="regular">
{{number (value-at-tl this.report.data level="3")}}
</LinkTo>
</td>
<td class="value">
<LinkTo @route="adminUsersList.show" @model="leader">
{{number (value-at-tl this.report.data level="4")}}
</LinkTo>
</td>

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,244 +0,0 @@
{{#unless this.isHidden}}
{{#if this.isEnabled}}
<ConditionalLoadingSection @isLoading={{this.isLoading}}>
{{#if this.showHeader}}
<div class="header">
{{#if this.showTitle}}
<ul class="breadcrumb">
{{#if this.showAllReportsLink}}
<li class="item all-reports">
<LinkTo @route="admin.dashboardReports" class="report-url">
{{i18n "admin.dashboard.all_reports"}}
</LinkTo>
</li>
{{#unless this.showNotFoundError}}
<li class="item separator">|</li>
{{/unless}}
{{/if}}
{{#unless this.showNotFoundError}}
<li class="item report">
<a href={{this.model.reportUrl}} class="report-url">
{{this.model.title}}
</a>
{{#if this.model.description}}
{{#if this.model.description_link}}
<a
target="_blank"
rel="noopener noreferrer"
href={{this.model.description_link}}
class="info"
data-tooltip={{this.model.description}}
>
{{d-icon "question-circle"}}
</a>
{{else}}
<span
class="info"
data-tooltip={{this.model.description}}
>
{{d-icon "question-circle"}}
</span>
{{/if}}
{{/if}}
</li>
{{/unless}}
</ul>
{{/if}}
{{#if this.shouldDisplayTrend}}
<div class="trend {{this.model.trend}}">
<span class="value" title={{this.model.trendTitle}}>
{{#if this.model.average}}
{{number this.model.currentAverage}}{{#if
this.model.percent
}}%{{/if}}
{{else}}
{{number this.model.currentTotal noTitle="true"}}{{#if
this.model.percent
}}%{{/if}}
{{/if}}
{{#if this.model.trendIcon}}
{{d-icon this.model.trendIcon class="icon"}}
{{/if}}
</span>
</div>
{{/if}}
</div>
{{/if}}
<div class="body">
<div class="main">
{{#if this.showError}}
{{#if this.showTimeoutError}}
<div class="alert alert-error report-alert timeout">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.timeout_error"}}</span>
</div>
{{/if}}
{{#if this.showExceptionError}}
<div class="alert alert-error report-alert exception">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.exception_error"}}</span>
</div>
{{/if}}
{{#if this.showNotFoundError}}
<div class="alert alert-error report-alert not-found">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.not_found_error"}}</span>
</div>
{{/if}}
{{else}}
{{#if this.hasData}}
{{#if this.currentMode}}
{{component
this.modeComponent
model=this.model
options=this.options
}}
{{#if this.model.relatedReport}}
<AdminReport
@showFilteringUI={{false}}
@dataSourceName={{this.model.relatedReport.type}}
/>
{{/if}}
{{/if}}
{{else}}
{{#if this.rateLimitationString}}
<div class="alert alert-error report-alert rate-limited">
{{d-icon "thermometer-three-quarters"}}
<span>{{this.rateLimitationString}}</span>
</div>
{{else}}
<div class="alert alert-info report-alert no-data">
{{d-icon "chart-pie"}}
{{#if this.model.reportUrl}}
<a href={{this.model.reportUrl}} class="report-url">
<span>
{{#if this.model.title}}
{{this.model.title}}
{{/if}}
{{i18n "admin.dashboard.reports.no_data"}}
</span>
</a>
{{else}}
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
{{/if}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
{{#if this.showFilteringUI}}
<div class="filters">
{{#if this.showModes}}
<div class="modes">
{{#each this.displayedModes as |displayedMode|}}
<DButton
@action={{action "onChangeMode"}}
@actionParam={{displayedMode.mode}}
@class={{displayedMode.cssClass}}
@icon={{displayedMode.icon}}
/>
{{/each}}
</div>
{{/if}}
{{#if this.isChartMode}}
{{#if this.model.average}}
<span class="average-chart">
{{i18n "admin.dashboard.reports.average_chart_label"}}
</span>
{{/if}}
<div class="chart-groupings">
{{#each this.chartGroupings as |chartGrouping|}}
<DButton
@label={{chartGrouping.label}}
@action={{action "changeGrouping" chartGrouping.id}}
@class={{chartGrouping.class}}
@disabled={{chartGrouping.disabled}}
/>
{{/each}}
</div>
{{/if}}
{{#if this.showDatesOptions}}
<div class="control">
<span class="label">
{{i18n "admin.dashboard.reports.dates"}}
</span>
<div class="input">
<DateTimeInputRange
@from={{this.startDate}}
@to={{this.endDate}}
@onChange={{action "onChangeDateRange"}}
@showFromTime={{false}}
@showToTime={{false}}
/>
</div>
</div>
{{/if}}
{{#each this.model.available_filters as |filter|}}
<div class="control">
<span class="label">
{{i18n
(concat
"admin.dashboard.reports.filters." filter.id ".label"
)
}}
</span>
<div class="input">
{{component
(concat "report-filters/" filter.type)
model=this.model
filter=filter
applyFilter=(action "applyFilter")
}}
</div>
</div>
{{/each}}
<div class="control">
<div class="input">
<DButton
@class="btn-default export-csv-btn"
@action={{action "exportCsv"}}
@label="admin.export_csv.button_text"
@icon="download"
/>
</div>
</div>
{{#if this.showRefresh}}
<div class="control">
<div class="input">
<DButton
@class="refresh-report-btn btn-primary"
@action={{action "refreshReport"}}
@label="admin.dashboard.reports.refresh_report"
@icon="sync"
/>
</div>
</div>
{{/if}}
</div>
{{/if}}
</div>
</ConditionalLoadingSection>
{{else}}
<div class="alert alert-info">
{{html-safe this.disabledLabel}}
</div>
{{/if}}
{{/unless}}

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,114 +0,0 @@
<div class="user-field">
{{#if (or this.isEditing (not this.userField.id))}}
<AdminFormRow @label="admin.user_fields.type">
<ComboBox
@content={{this.fieldTypes}}
@value={{this.buffered.field_type}}
@onChange={{action (mut this.buffered.field_type)}}
/>
</AdminFormRow>
<AdminFormRow @label="admin.user_fields.name">
<Input
@value={{this.buffered.name}}
class="user-field-name"
maxlength="255"
/>
</AdminFormRow>
<AdminFormRow @label="admin.user_fields.description">
<Input
@value={{this.buffered.description}}
class="user-field-desc"
maxlength="255"
/>
</AdminFormRow>
{{#if this.bufferedFieldType.hasOptions}}
<AdminFormRow @label="admin.user_fields.options">
<ValueList @values={{this.buffered.options}} @inputType="array" />
</AdminFormRow>
{{/if}}
<AdminFormRow @wrapLabel="true">
<Input @type="checkbox" @checked={{this.buffered.editable}} />
<span>{{i18n "admin.user_fields.editable.title"}}</span>
</AdminFormRow>
<AdminFormRow @wrapLabel="true">
<Input @type="checkbox" @checked={{this.buffered.required}} />
<span>{{i18n "admin.user_fields.required.title"}}</span>
</AdminFormRow>
<AdminFormRow @wrapLabel="true">
<Input @type="checkbox" @checked={{this.buffered.show_on_profile}} />
<span>{{i18n "admin.user_fields.show_on_profile.title"}}</span>
</AdminFormRow>
<AdminFormRow @wrapLabel="true">
<Input @type="checkbox" @checked={{this.buffered.show_on_user_card}} />
<span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span>
</AdminFormRow>
<AdminFormRow @wrapLabel="true">
<Input @type="checkbox" @checked={{this.buffered.searchable}} />
<span>{{i18n "admin.user_fields.searchable.title"}}</span>
</AdminFormRow>
<AdminFormRow>
<DButton
@action={{action "save"}}
@class="btn-primary save"
@icon="check"
@label="admin.user_fields.save"
/>
<DButton
@action={{action "cancel"}}
@class="btn-danger cancel"
@icon="times"
@label="admin.user_fields.cancel"
/>
</AdminFormRow>
{{else}}
<div class="row">
<div class="form-display">
<b class="name">{{this.userField.name}}</b>
<br />
<span class="description">{{html-safe
this.userField.description
}}</span>
</div>
<div class="form-display field-type">{{this.fieldName}}</div>
<div class="form-element controls">
<DButton
@action={{action "edit"}}
@class="btn-default edit"
@icon="pencil-alt"
@label="admin.user_fields.edit"
/>
<DButton
@action={{this.destroyAction}}
@actionParam={{this.userField}}
@class="btn-danger cancel"
@icon="far-trash-alt"
@label="admin.user_fields.delete"
/>
<DButton
@action={{this.moveUpAction}}
@actionParam={{this.userField}}
@class="btn-default"
@icon="arrow-up"
@disabled={{this.cantMoveUp}}
/>
<DButton
@action={{this.moveDownAction}}
@actionParam={{this.userField}}
@class="btn-default"
@icon="arrow-down"
@disabled={{this.cantMoveDown}}
/>
</div>
</div>
<div class="row">{{this.flags}}</div>
{{/if}}
</div>

View File

@ -1,20 +0,0 @@
<span
role="button"
onclick={{action "deleteWord"}}
class="delete-word-record"
>{{d-icon "times"}}</span>
{{this.word.word}}
{{#if (or this.isReplace this.isLink)}}
&rarr;
<span class="replacement">{{this.word.replacement}}</span>
{{else if this.isTag}}
&rarr;
{{#each this.tags as |tag|}}
<span class="tag">{{tag}}</span>
{{/each}}
{{/if}}
{{#if this.isCaseSensitive}}
<span class="case-sensitive">{{i18n
"admin.watched_words.case_sensitive"
}}</span>
{{/if}}

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

@ -0,0 +1,47 @@
import Component from "@ember/component";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any((event) => event.name === typeName);
},
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter((eventType) => eventType.name === type.name)
);
}
}
return value;
},
},
});

View File

@ -0,0 +1,110 @@
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import Component from "@ember/component";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
dialog: service(),
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) {
return "";
}
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},
actions: {
redeliver() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.events.redeliver_confirm"),
didConfirm: () => {
return ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then((json) => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
},
});
},
toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;
if (this.expandDetails !== expandDetailsKey) {
let headers = Object.assign(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST",
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload"),
});
} else {
this.set("expandDetails", null);
}
},
toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;
if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body"),
});
} else {
this.set("expandDetails", null);
}
},
},
});

View File

@ -0,0 +1,39 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find((s) => s.id === lastDeliveryStatus);
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
htmlSafe(iconHTML(this.icon, { class: this.class }))
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
},
});

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,12 +0,0 @@
{{#if this.onlyHex}}<span class="add-on">#</span>{{/if}}<TextField
@class="hex-input"
@value={{this.hexValue}}
@maxlength={{this.maxlength}}
@input={{action "onHexInput" value="target.value"}}
/>
<input
class="picker"
type="color"
value={{this.normalizedHexValue}}
{{on "input" this.onPickerInput}}
/>

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,17 +0,0 @@
<div class="admin-new-feature-item">
<div class="new-feature-emoji">{{this.item.emoji}}</div>
<div class="new-feature-content">
<div class="header">
{{#if this.item.link}}
<a
href={{this.item.link}}
target="_blank"
rel="noopener noreferrer"
>{{this.item.title}}</a>
{{else}}
{{this.item.title}}
{{/if}}
</div>
<div class="feature-description">{{this.item.description}}</div>
</div>
</div>

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,28 +0,0 @@
{{#if this.newFeatures}}
<div class="section-title">
<h2>{{replace-emoji (i18n "admin.dashboard.new_features.title")}}</h2>
</div>
<div class="section-body {{this.columnCountClass}}">
{{#each this.newFeatures as |feature|}}
<DashboardNewFeatureItem @item={{feature}} @tagName="" />
{{/each}}
</div>
<div class="section-footer">
{{#if this.releaseNotesLink}}
<a
rel="noopener noreferrer"
target="_blank"
href={{this.releaseNotesLink}}
class="btn btn-primary new-features-release-notes"
>
{{i18n "admin.dashboard.new_features.learn_more"}}
</a>
{{/if}}
<DButton
@label="admin.dashboard.new_features.dismiss"
@class="btn-default new-features-dismiss"
@action={{this.dismissNewFeatures}}
/>
</div>
{{/if}}

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,58 +0,0 @@
{{#if this.foundProblems}}
<div class="section dashboard-problems">
<div class="section-title">
<h2>
{{d-icon "heart"}}
{{i18n "admin.dashboard.problems_found"}}
</h2>
</div>
<div class="section-body">
<ConditionalLoadingSection @isLoading={{this.loadingProblems}}>
{{#if this.highPriorityProblems.length}}
<div class="problem-messages priority-high">
<ul>
{{#each this.highPriorityProblems as |problem|}}
<li
class={{concat
"dashboard-problem "
"priority-"
problem.priority
}}
>
{{d-icon "exclamation-triangle"}}
{{html-safe problem.message}}
</li>
{{/each}}
</ul>
</div>
{{/if}}
<div class="problem-messages priority-low">
<ul>
{{#each this.lowPriorityProblems as |problem|}}
<li
class={{concat
"dashboard-problem "
"priority-"
problem.priority
}}
>{{html-safe problem.message}}</li>
{{/each}}
</ul>
</div>
<p class="actions">
<DButton
@action={{this.refreshProblems}}
@class="btn-default"
@icon="sync"
@label="admin.dashboard.refresh_problems"
/>
{{i18n "admin.dashboard.last_checked"}}:
{{this.problemsTimestamp}}
</p>
</ConditionalLoadingSection>
</div>
</div>
{{/if}}

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,15 +0,0 @@
{{#if this.isCheckbox}}
<label for={{this.inputId}}>
<Input @checked={{this.checked}} id={{this.inputId}} @type="checkbox" />
{{i18n this.translationKey}}
</label>
{{else}}
<label for={{this.inputId}}>{{i18n this.translationKey}}</label>
<Input
@value={{this.value}}
id={{this.inputId}}
placeholder={{this.placeholder}}
/>
{{/if}}
<div class="clearfix"></div>

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,63 +0,0 @@
{{#if this.collection}}
<ul class="values emoji-value-list">
{{#each this.collection as |data index|}}
<li class="value" data-index={{index}}>
<DButton
@action={{action "removeValue"}}
@actionParam={{data}}
@icon="times"
@class="remove-value-btn btn-small"
@disabled={{not data.isEditable}}
/>
<div
class="value-input emoji-details
{{if data.isEditable 'can-edit'}}
{{if data.isEditing 'd-editor-textarea-wrapper'}}"
{{on "click" (fn this.editValue index)}}
role="button"
>
<img
height="15px"
width="15px"
src={{data.emojiUrl}}
class="emoji-list-emoji"
/>
<span class="emoji-name">{{data.value}}</span>
</div>
{{#if this.showUpDownButtons}}
<DButton
@action={{action "shift" -1 index}}
@icon="arrow-up"
@class="shift-up-value-btn btn-small"
/>
<DButton
@action={{action "shift" 1 index}}
@icon="arrow-down"
@class="shift-down-value-btn btn-small"
/>
{{/if}}
</li>
{{/each}}
</ul>
{{/if}}
<div class="value">
<DButton
@action={{action "editValue"}}
@actionParam={{this.data}}
@icon="discourse-emojis"
@class="add-emoji-button d-editor-textarea-wrapper"
@label="admin.site_settings.emoji_list.add_emoji_button.label"
/>
</div>
<EmojiPicker
@isActive={{this.emojiPickerIsActive}}
@isEditorFocused={{this.isEditorFocused}}
@emojiSelected={{action "emojiSelected"}}
@onEmojiPickerClose={{action "closeEmojiPicker"}}
/>
<SettingValidationMessage @message={{this.validationMessage}} />

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,43 +0,0 @@
<div class="flagged-by">
<div class="user-list-title">
{{i18n "admin.flags.flagged_by"}}
</div>
<div class="flag-users">
{{#each this.flaggedPost.post_actions as |postAction|}}
<FlagUser @user={{postAction.user}} @date={{postAction.created_at}}>
<div class="flagger-flag-type">
{{post-action-title
postAction.post_action_type_id
postAction.name_key
}}
</div>
<UserFlagPercentage
@agreed={{postAction.user.flags_agreed}}
@disagreed={{postAction.user.flags_disagreed}}
@ignored={{postAction.user.flags_ignored}}
/>
</FlagUser>
{{/each}}
</div>
</div>
{{#if this.showResolvedBy}}
<div class="flagged-post-resolved-by">
<div class="user-list-title">
{{i18n "admin.flags.resolved_by"}}
</div>
<div class="flag-users">
{{#each this.flaggedPost.post_actions as |postAction|}}
<FlagUser
@user={{postAction.disposed_by}}
@date={{postAction.disposed_at}}
>
{{disposition-icon postAction.disposition}}
{{#if postAction.staff_took_action}}
{{d-icon "gavel" title="admin.flags.took_action"}}
{{/if}}
</FlagUser>
{{/each}}
</div>
</div>
{{/if}}

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