Version bump

This commit is contained in:
Blake Erickson 2023-03-16 17:54:40 -06:00
commit 4dd8f6e486
2175 changed files with 56350 additions and 27800 deletions

View File

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

View File

@ -1,76 +0,0 @@
name: Documentation
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
build:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
name: run
runs-on: ubuntu-latest
container: discourse/discourse_test:slim
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Git
run: |
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- name: Bundler cache
uses: actions/cache@v3
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Setup gems
run: |
gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock)
bundle config --local path vendor/bundle
bundle config --local deployment true
bundle config --local without development
bundle install --jobs 4
bundle clean
- name: Get yarn cache directory
id: yarn-cache-dir
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install
run: yarn install
- name: Check Chat documentation
run: |
LOAD_PLUGINS=1 bin/rake chat:doc
if [ ! -z "$(git status --porcelain plugins/chat/docs/)" ]; then
echo "Chat documentation is not up to date. To resolve, run:"
echo " LOAD_PLUGINS=1 bin/rake chat:doc"
echo
echo "Or manually apply the diff printed below:"
echo "---------------------------------------------"
git -c color.ui=always diff plugins/chat/docs/
exit 1
fi
timeout-minutes: 30

View File

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

View File

@ -20,7 +20,7 @@ jobs:
if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
name: ${{ matrix.target }} ${{ matrix.build_type }} ${{ matrix.ruby }}
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.2') && '-ruby-3.2.0' || '' }}
container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }}${{ (matrix.ruby == '3.1') && '-ruby-3.1.0' || '' }}
timeout-minutes: 20
env:
@ -29,7 +29,7 @@ jobs:
RAILS_ENV: test
PGUSER: discourse
PGPASSWORD: discourse
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' }}
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' || matrix.build_type == 'system' }}
CAPBYARA_DEFAULT_MAX_WAIT_TIME: 4
strategy:
@ -38,7 +38,7 @@ jobs:
matrix:
build_type: [backend, frontend, system, annotations]
target: [core, plugins]
ruby: ['3.1']
ruby: ['3.2']
exclude:
- build_type: annotations
target: plugins
@ -46,6 +46,9 @@ jobs:
target: core # Handled by core_frontend_tests job (below)
steps:
- name: Set working directory owner
run: chown root:root .
- uses: actions/checkout@v3
with:
fetch-depth: 1
@ -156,6 +159,22 @@ jobs:
path: tmp/turbo_rspec_runtime.log
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
if: matrix.build_type == 'backend' && matrix.target == 'core'
run: bin/turbo_rspec --verbose
@ -173,13 +192,17 @@ jobs:
if: matrix.build_type == 'system'
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
if: matrix.build_type == 'system' && matrix.target == 'core'
run: bin/rspec spec/system --format documentation --profile
run: bin/rspec spec/system
- name: Plugin System Tests
if: matrix.build_type == 'system' && matrix.target == 'plugins'
run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system --format documentation --profile
run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system
- name: Upload failed system test screenshots
uses: actions/upload-artifact@v3

14
.jsdoc
View File

@ -3,5 +3,19 @@
{
"source": {
"excludePattern": ""
},
"templates": {
"default": {
"includeDate": false
}
},
"opts": {
"template": "./node_modules/tidy-jsdoc",
"prism-theme": "prism-custom",
"encoding": "utf8",
"recurse": true
},
"metadata": {
"title": "Discourse"
}
}

View File

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

View File

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

View File

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

View File

@ -7,3 +7,7 @@ Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
Discourse/NoResetColumnInformationInMigrations:
Enabled: true
Lint/Debugger:
Exclude:
- script/**/*

View File

@ -1 +1 @@
3.1.3
3.2.1

View File

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

14
Gemfile
View File

@ -18,7 +18,7 @@ else
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
rails_version = "7.0.4.1"
rails_version = "7.0.4.3"
gem "actionmailer", rails_version
gem "actionpack", rails_version
gem "actionview", rails_version
@ -105,7 +105,7 @@ gem "pg"
gem "mini_sql"
gem "pry-rails", require: false
gem "pry-byebug", require: false
gem "r2", require: false
gem "rtlcss", require: false
gem "rake"
gem "thor", require: false
@ -180,6 +180,7 @@ group :development do
gem "better_errors", platform: :mri, require: !!ENV["BETTER_ERRORS"]
gem "binding_of_caller"
gem "yaml-lint"
gem "yard"
end
if ENV["ALLOW_DEV_POPULATE"] == "1"
@ -229,10 +230,9 @@ gem "logstash-event", require: false
gem "logstash-logger", require: false
gem "logster"
# NOTE: later versions of sassc are causing a segfault, possibly dependent on processer architecture
# and until resolved should be locked at 2.0.1
gem "sassc", "2.0.1", require: false
gem "sassc-rails"
# These are forks of sassc and sassc-rails with dart-sass support
gem "dartsass-ruby"
gem "dartsass-sprockets"
gem "rotp", require: false
@ -279,3 +279,5 @@ 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

@ -17,25 +17,25 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actionmailer (7.0.4.1)
actionpack (= 7.0.4.1)
actionview (= 7.0.4.1)
activejob (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionmailer (7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activesupport (= 7.0.4.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.4.1)
actionview (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionpack (7.0.4.3)
actionview (= 7.0.4.3)
activesupport (= 7.0.4.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (7.0.4.1)
activesupport (= 7.0.4.1)
actionview (7.0.4.3)
activesupport (= 7.0.4.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -44,15 +44,15 @@ GEM
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (7.0.4.1)
activesupport (= 7.0.4.1)
activejob (7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.3.6)
activemodel (7.0.4.1)
activesupport (= 7.0.4.1)
activerecord (7.0.4.1)
activemodel (= 7.0.4.1)
activesupport (= 7.0.4.1)
activesupport (7.0.4.1)
activemodel (7.0.4.3)
activesupport (= 7.0.4.3)
activerecord (7.0.4.3)
activemodel (= 7.0.4.3)
activesupport (= 7.0.4.3)
activesupport (7.0.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -88,7 +88,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.15.0)
bootsnap (1.16.0)
msgpack (~> 1.2)
builder (3.2.4)
bullet (7.0.7)
@ -110,7 +110,7 @@ GEM
chunky_png (1.4.0)
coderay (1.1.3)
colored2 (3.1.2)
concurrent-ruby (1.2.0)
concurrent-ruby (1.2.2)
connection_pool (2.3.0)
cose (1.3.0)
cbor (~> 0.5.9)
@ -121,6 +121,14 @@ GEM
crass (1.0.6)
css_parser (1.14.0)
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)
diff-lcs (1.5.0)
@ -138,7 +146,7 @@ GEM
regexp_parser (~> 2.2)
email_reply_trimmer (0.1.13)
erubi (1.12.0)
excon (0.97.2)
excon (0.99.0)
execjs (2.8.1)
exifr (1.3.10)
fabrication (2.30.0)
@ -149,7 +157,7 @@ GEM
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.2)
faraday-retry (2.0.0)
faraday-retry (2.1.0)
faraday (~> 2.0)
fast_blank (1.0.1)
fast_xs (0.8.0)
@ -157,8 +165,13 @@ GEM
ffi (1.15.5)
fspath (3.1.2)
gc_tracer (1.5.1)
globalid (1.0.1)
globalid (1.1.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)
hana (1.3.7)
hashdiff (1.0.1)
@ -169,7 +182,7 @@ GEM
http_accept_language (2.1.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
image_optim (0.31.2)
image_optim (0.31.3)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (>= 1.5, < 4)
@ -186,7 +199,7 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.6.0)
jwt (2.7.0)
kgio (2.11.4)
libv8-node (16.10.0.0)
libv8-node (16.10.0.0-aarch64-linux)
@ -206,7 +219,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.11.3)
logster (2.12.2)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -227,10 +240,10 @@ GEM
mini_sql (1.4.0)
mini_suffix (0.3.3)
ffi (~> 1.9)
minitest (5.17.0)
minitest (5.18.0)
mocha (2.0.2)
ruby2_keywords (>= 0.0.5)
msgpack (1.6.0)
msgpack (1.6.1)
multi_json (1.15.0)
multi_xml (0.6.0)
mustache (1.1.1)
@ -246,16 +259,16 @@ GEM
net-smtp (0.3.3)
net-protocol
nio4r (2.5.8)
nokogiri (1.14.0)
nokogiri (1.14.2)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.14.0-aarch64-linux)
nokogiri (1.14.2-aarch64-linux)
racc (~> 1.4)
nokogiri (1.14.0-arm64-darwin)
nokogiri (1.14.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.0-x86_64-darwin)
nokogiri (1.14.2-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.14.0-x86_64-linux)
nokogiri (1.14.2-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@ -292,17 +305,17 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
openssl (3.0.2)
openssl-signature_algorithm (1.2.1)
openssl (> 2.0, < 3.1)
openssl (3.1.0)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
optimist (3.0.1)
parallel (1.22.1)
parallel_tests (4.1.0)
parallel_tests (4.2.0)
parallel
parser (3.2.0.0)
parser (3.2.1.1)
ast (~> 2.4.1)
pg (1.4.5)
prettier_print (1.2.0)
pg (1.4.6)
prettier_print (1.2.1)
progress (3.6.0)
pry (0.14.2)
coderay (~> 1.1)
@ -313,16 +326,15 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.0.2)
puma (6.1.1)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.6.2)
rack (2.2.6.2)
rack (2.2.6.4)
rack-mini-profiler (3.0.0)
rack (>= 1.2.0)
rack-protection (3.0.5)
rack
rack-test (2.0.2)
rack-test (2.1.0)
rack (>= 1.3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
@ -336,15 +348,15 @@ GEM
rails_multisite (4.0.1)
activerecord (> 5.0, < 7.1)
railties (> 5.0, < 7.1)
railties (7.0.4.1)
actionpack (= 7.0.4.1)
activesupport (= 7.0.4.1)
railties (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
raindrops (0.20.0)
raindrops (0.20.1)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
@ -354,10 +366,10 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.8.0)
redis (4.8.1)
redis-namespace (1.10.0)
redis (>= 4)
regexp_parser (2.6.2)
regexp_parser (2.7.0)
request_store (1.5.1)
rack (>= 1.4)
rexml (3.2.5)
@ -371,7 +383,7 @@ GEM
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.0)
rspec-core (3.12.1)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
diff-lcs (>= 1.2.0, < 2.0)
@ -379,7 +391,7 @@ GEM
rspec-html-matchers (0.10.0)
nokogiri (~> 1)
rspec (>= 3.0.0.a)
rspec-mocks (3.12.3)
rspec-mocks (3.12.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
@ -398,46 +410,50 @@ GEM
json-schema (>= 2.2, < 4.0)
railties (>= 3.1, < 7.1)
rspec-core (>= 2.14)
rubocop (1.44.0)
rtlcss (0.2.0)
mini_racer (~> 0.6.3)
rubocop (1.48.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.24.1, < 2.0)
rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.24.1)
parser (>= 3.1.1.0)
rubocop-capybara (2.17.0)
rubocop-ast (1.27.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.17.1)
rubocop (~> 1.41)
rubocop-discourse (3.0.3)
rubocop-discourse (3.2.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.18.1)
rubocop-rspec (2.19.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
ruby-prof (1.4.5)
ruby-progressbar (1.11.0)
ruby-prof (1.6.1)
ruby-progressbar (1.13.0)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sanitize (6.0.0)
sanitize (6.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sassc (2.0.1)
ffi (~> 1.9)
rake
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (4.8.0)
sass-embedded (1.59.2)
google-protobuf (~> 3.21)
rake (>= 10.0.0)
sass-embedded (1.59.2-aarch64-linux-gnu)
google-protobuf (~> 3.21)
sass-embedded (1.59.2-arm64-darwin)
google-protobuf (~> 3.21)
sass-embedded (1.59.2-x86_64-darwin)
google-protobuf (~> 3.21)
sass-embedded (1.59.2-x86_64-linux-gnu)
google-protobuf (~> 3.21)
selenium-webdriver (4.8.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@ -462,15 +478,17 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.23)
syntax_tree (5.2.0)
syntax_tree (6.0.2)
prettier_print (>= 1.2.0)
syntax_tree-disable_ternary (1.0.0)
test-prof (1.1.0)
test-prof (1.2.0)
thor (1.2.1)
tilt (2.0.11)
timeout (0.3.1)
tzinfo (2.0.5)
tilt (2.1.0)
timeout (0.3.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2022.7)
tzinfo (>= 1.0.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
@ -502,7 +520,9 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yaml-lint (0.1.2)
zeitwerk (2.6.6)
yard (0.9.28)
webrick (~> 1.7.0)
zeitwerk (2.6.7)
PLATFORMS
aarch64-linux
@ -514,14 +534,14 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
actionmailer (= 7.0.4.1)
actionpack (= 7.0.4.1)
actionview (= 7.0.4.1)
actionmailer (= 7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 7.0.4.1)
activerecord (= 7.0.4.1)
activesupport (= 7.0.4.1)
activemodel (= 7.0.4.3)
activerecord (= 7.0.4.3)
activesupport (= 7.0.4.3)
addressable
annotate
aws-sdk-s3
@ -539,6 +559,8 @@ DEPENDENCIES
cose
cppjieba_rb
css_parser
dartsass-ruby
dartsass-sprockets
diffy
digest
discourse-fonts
@ -600,13 +622,12 @@ DEPENDENCIES
pry-byebug
pry-rails
puma
r2
rack
rack-mini-profiler
rack-protection
rails_failover
rails_multisite
railties (= 7.0.4.1)
railties (= 7.0.4.3)
rake
rb-fsevent
rbtrace
@ -621,13 +642,12 @@ DEPENDENCIES
rspec-rails
rss
rswag-specs
rtlcss
rubocop-discourse
ruby-prof
ruby-readability
rubyzip
sanitize
sassc (= 2.0.1)
sassc-rails
selenium-webdriver
shoulda-matchers
sidekiq
@ -640,6 +660,7 @@ DEPENDENCIES
syntax_tree-disable_ternary
test-prof
thor
tzinfo-data
uglifier
unf
unicorn
@ -649,6 +670,7 @@ DEPENDENCIES
webrick
xorcist
yaml-lint
yard
BUNDLED WITH
2.4.1
2.4.4

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.
Before you get started, ensure you have the following minimum versions: [Ruby 3.1+](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 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!
## Setting up Discourse

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,43 +1,46 @@
import { tagName } from "@ember-decorators/component";
import { equal } from "@ember/object/computed";
import Component from "@ember/component";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
const CUSTOM_REASON_KEY = "custom";
export default Component.extend({
tagName: "",
selectedReason: CUSTOM_REASON_KEY,
customReason: "",
reasonKeys: [
@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,
],
isCustomReason: equal("selectedReason", 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) {
@ -48,5 +51,5 @@ export default Component.extend({
I18n.t(`admin.user.suspend_reasons.${this.selectedReason}`)
);
}
},
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { classNames } from "@ember-decorators/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,4 +1,5 @@
import { tagName } from "@ember-decorators/component";
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});
@tagName("tr")
export default class AdminReportPerDayCounts extends Component {}

View File

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

View File

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

View File

@ -1,21 +1,27 @@
import Component from "@ember/component";
import {
attributeBindings,
classNameBindings,
classNames,
tagName,
} from "@ember-decorators/component";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
tagName: "td",
classNames: ["admin-report-table-cell"],
classNameBindings: ["type", "property"],
attributeBindings: ["value:title"],
options: null,
@tagName("td")
@classNames("admin-report-table-cell")
@classNameBindings("type", "property")
@attributeBindings("value:title")
export default class AdminReportTableCell extends Component {
options = null;
@alias("label.type") type;
@alias("label.mainProperty") property;
@alias("computedLabel.formattedValue") formattedValue;
@alias("computedLabel.value") value;
@discourseComputed("label", "data", "options")
computedLabel(label, 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,19 +1,24 @@
import {
attributeBindings,
classNameBindings,
classNames,
tagName,
} from "@ember-decorators/component";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
tagName: "th",
classNames: ["admin-report-table-header"],
classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
attributeBindings: ["label.title:title"],
@tagName("th")
@classNames("admin-report-table-header")
@classNameBindings("label.mainProperty", "label.type", "isCurrentSort")
@attributeBindings("label.title:title")
export default class AdminReportTableHeader extends Component {
@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
isCurrentSort(currentSortField, labelSortField) {
return currentSortField === labelSortField;
},
}
@discourseComputed("currentSortDirection")
sortIcon(currentSortDirection) {
return currentSortDirection === 1 ? "caret-up" : "caret-down";
},
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@
@checked={{this.onlyOverridden}}
{{on
"click"
(action "onlyOverriddenChanged" value="target.checked")
(action this.onlyOverriddenChanged value="target.checked")
}}
/>
{{i18n "admin.customize.theme.hide_unused_fields"}}
@ -125,6 +125,6 @@
@autofocus="true"
@placeholder={{this.placeholder}}
@htmlPlaceholder={{true}}
@save={{action "save"}}
@save={{this.save}}
@setWarning={{action "setWarning"}}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
@content={{this.editorContents}}
@mode={{this.currentEditorMode}}
@editorId={{this.editorId}}
@save={{action "save"}}
@save={{@save}}
/>
<div class="admin-footer">

View File

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

View File

@ -9,15 +9,6 @@
autofocus={{true}}
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.class_name"}}</div>
<Input
@value={{this.buffered.class_name}}
placeholder="class"
@enter={{action "save"}}
class="class-name"
/>
</td>
<td class="editing-input">
<div class="label">{{i18n "admin.embedding.allowed_paths"}}</div>
<Input
@ -54,12 +45,6 @@
<div class="label">{{i18n "admin.embedding.host"}}</div>
{{this.host.host}}
</td>
<td>
<div class="label">
{{i18n "admin.embedding.class_name"}}
</div>
{{this.host.class_name}}
</td>
<td>
<div class="label">
{{i18n "admin.embedding.allowed_paths"}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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