Version bump

# Conflicts:
#	app/assets/javascripts/discourse/app/widgets/post-cooked.js
#	db/migrate/20220920044310_enforce_user_profile_max_limits.rb
#	spec/requests/admin/themes_controller_spec.rb
This commit is contained in:
Jarek Radosz 2022-09-29 20:41:00 +02:00
commit d4adf6fa66
No known key found for this signature in database
GPG Key ID: 62D0FBAE5BF9B953
1432 changed files with 66353 additions and 40440 deletions

View File

@ -2,7 +2,8 @@
"extends": "eslint-config-discourse",
"rules": {
"discourse-ember/global-ember": 2,
"eol-last": 2
"eol-last": 2,
"no-restricted-globals": 0
},
"globals": {
"_": "off",

View File

@ -53,5 +53,8 @@ ce3fe2f4c4ddf166949ee3cec3d9ecbf9108ab52
# DEV: Tidy up imports. (#11364)
1c2358ba162eb9f9ba9095c9afe30cf51dd85e04
# DEV: Sort imports alphabetically (#11382)
# DEV: Sort imports alphabetically (#11382)
bbe5d8d5cf1220165842985c0e2cd4c454d501cd
# DEV: Template colocation for sidebar files
95c7cdab941a56686ac5831d2a5c5eca38d780c5

View File

@ -1,42 +1,50 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
time: "08:00"
timezone: Australia/Sydney
open-pull-requests-limit: 10
versioning-strategy: lockfile-only
allow:
- dependency-type: direct
- dependency-type: indirect
ignore:
- dependency-name: aws-partitions
versions:
- "> 1.329.0"
- "< 2"
- dependency-name: aws-sdk-core
versions:
- "> 3.99.1"
- "< 4"
- dependency-name: aws-sdk-kms
versions:
- "> 1.31.0"
- "< 2"
- dependency-name: aws-sdk-s3
versions:
- "> 1.66.0"
- "< 2"
- dependency-name: aws-sdk-sns
versions:
- "> 1.25.1"
- "< 2"
- dependency-name: aws-sigv4
versions:
- "> 1.2.0"
- "< 2"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
time: "08:00"
timezone: Australia/Sydney
open-pull-requests-limit: 10
versioning-strategy: lockfile-only
allow:
- dependency-type: direct
- dependency-type: indirect
ignore:
- dependency-name: aws-partitions
versions:
- "> 1.329.0"
- "< 2"
- dependency-name: aws-sdk-core
versions:
- "> 3.99.1"
- "< 4"
- dependency-name: aws-sdk-kms
versions:
- "> 1.31.0"
- "< 2"
- dependency-name: aws-sdk-s3
versions:
- "> 1.66.0"
- "< 2"
- dependency-name: aws-sdk-sns
versions:
- "> 1.25.1"
- "< 2"
- dependency-name: aws-sigv4
versions:
- "> 1.2.0"
- "< 2"
- package-ecosystem: "npm"
directory: "/app/assets/javascripts/"
schedule:
interval: daily
time: "08:00"
timezone: Australia/Sydney
open-pull-requests-limit: 20
versioning-strategy: increase

View File

@ -67,7 +67,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Check RubyGems Licenses
if: ${{ always() }}
if: ${{ !cancelled() }}
run: |
licensed cache
licensed status
@ -76,14 +76,14 @@ jobs:
run: yarn install
- name: Check Yarn Licenses
if: ${{ always() }}
if: ${{ !cancelled() }}
run: |
yarn global add licensee
yarn global upgrade licensee
licensee --errors-only
- name: Check Ember CLI Workspace Licenses
if: ${{ always() }}
if: ${{ !cancelled() }}
working-directory: ./app/assets/javascripts
run: |
licensee --errors-only

View File

@ -64,19 +64,19 @@ jobs:
run: yarn install
- name: Rubocop
if: ${{ always() }}
if: ${{ !cancelled() }}
run: bundle exec rubocop --parallel .
- name: ESLint (core)
if: ${{ always() }}
if: ${{ !cancelled() }}
run: yarn eslint app/assets/javascripts
- name: ESLint (core plugins)
if: ${{ always() }}
if: ${{ !cancelled() }}
run: yarn eslint plugins
- name: Prettier
if: ${{ always() }}
if: ${{ !cancelled() }}
run: |
yarn prettier -v
yarn pprettier --list-different \
@ -86,7 +86,7 @@ jobs:
"plugins/**/assets/javascripts/**/*.js"
- name: Ember template lint
if: ${{ always() }}
if: ${{ !cancelled() }}
run: |
yarn ember-template-lint \
--no-error-on-unmatched-pattern \
@ -94,9 +94,9 @@ jobs:
"plugins/**/assets/javascripts/**/*.hbs"
- name: English locale lint (core)
if: ${{ always() }}
if: ${{ !cancelled() }}
run: bundle exec ruby script/i18n_lint.rb "config/**/locales/{client,server}.en.yml"
- name: English locale lint (core plugins)
if: ${{ always() }}
if: ${{ !cancelled() }}
run: bundle exec ruby script/i18n_lint.rb "plugins/**/locales/{client,server}.en.yml"

View File

@ -18,9 +18,9 @@ permissions:
jobs:
build:
name: ${{ matrix.target }} ${{ matrix.build_type }}
runs-on: ubuntu-latest
container: discourse/discourse_test:slim${{ startsWith(matrix.build_type, 'frontend') && '-browsers' || '' }}
timeout-minutes: 60
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' || '' }}
timeout-minutes: 20
env:
DISCOURSE_HOSTNAME: www.example.com
@ -28,19 +28,21 @@ jobs:
RAILS_ENV: test
PGUSER: discourse
PGPASSWORD: discourse
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' && matrix.target == 'core' }}
USES_PARALLEL_DATABASES: ${{ matrix.build_type == 'backend' }}
strategy:
fail-fast: false
matrix:
build_type: [backend, frontend, annotations]
build_type: [backend, frontend, system, annotations]
target: [core, plugins]
exclude:
- build_type: annotations
target: plugins
- build_type: frontend
target: core # Handled by core_frontend_tests job (below)
- build_type: system
target: plugins # Enable once at least 1 plugin has system tests
steps:
- uses: actions/checkout@v3
@ -145,19 +147,46 @@ jobs:
if: steps.app-cache.outputs.cache-hit != 'true'
run: rm -rf tmp/app-cache/uploads && cp -r public/uploads tmp/app-cache/uploads
- name: Fetch turbo_rspec_runtime.log cache
uses: actions/cache@v3
id: test-runtime-cache
if: matrix.build_type == 'backend' && matrix.target == 'core'
with:
path: tmp/turbo_rspec_runtime.log
key: rspec-runtime-backend-core
- name: Core RSpec
if: matrix.build_type == 'backend' && matrix.target == 'core'
run: bin/turbo_rspec --verbose
- name: Plugin RSpec
if: matrix.build_type == 'backend' && matrix.target == 'plugins'
run: bin/rake plugin:spec
run: bin/rake plugin:turbo_spec
- name: Plugin QUnit
if: matrix.build_type == 'frontend' && matrix.target == 'plugins'
run: bin/rake plugin:qunit['*','1200000']
run: QUNIT_PARALLEL=3 bin/rake plugin:qunit['*','1200000']
timeout-minutes: 30
- name: Ember Build for System Tests
if: matrix.build_type == 'system'
run: bin/ember-cli --build
- name: Core System Tests
if: matrix.build_type == 'system' && matrix.target == 'core'
run: bin/system_rspec
- name: Plugin System Tests
if: matrix.build_type == 'system' && matrix.target == 'plugins'
run: bin/system_rspec plugins/*/spec/system
- name: Upload failed system test screenshots
uses: actions/upload-artifact@v3
if: matrix.build_type == 'system' && failure()
with:
name: failed-system-test-screenshots
path: tmp/screenshots/*.png
- name: Check Annotations
if: matrix.build_type == 'annotations'
run: |
@ -177,14 +206,21 @@ jobs:
core_frontend_tests:
name: core frontend (${{ matrix.browser }})
runs-on: ubuntu-latest
container: discourse/discourse_test:slim-browsers
timeout-minutes: 30
runs-on: ubuntu-20.04-8core
container:
image: discourse/discourse_test:slim-browsers
options: --user discourse
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
browser: ["Chrome", "Firefox", "Headless Firefox"]
browser: ["Chrome", "Firefox ESR", "Firefox Evergreen"]
env:
TESTEM_BROWSER: ${{ (startsWith(matrix.browser, 'Firefox') && 'Firefox') || matrix.browser }}
TESTEM_FIREFOX_PATH: ${{ (matrix.browser == 'Firefox Evergreen') && '/opt/firefox-evergreen/firefox' }}
steps:
- uses: actions/checkout@v2
@ -216,23 +252,16 @@ jobs:
- name: Ember Build
working-directory: ./app/assets/javascripts/discourse
run: |
sudo -E -u discourse mkdir /tmp/emberbuild
sudo -E -u discourse -H yarn ember build --environment=test -o /tmp/emberbuild
mkdir /tmp/emberbuild
yarn ember build --environment=test -o /tmp/emberbuild
- name: Core QUnit 1
if: ${{ always() }}
- name: Core QUnit
working-directory: ./app/assets/javascripts/discourse
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}" --random
timeout-minutes: 20
run: yarn ember exam --path /tmp/emberbuild --load-balance --parallel=5 --launch "${{ env.TESTEM_BROWSER }}" --write-execution-file --random
timeout-minutes: 15
- name: Core QUnit 2
- uses: actions/upload-artifact@v3
if: ${{ always() }}
working-directory: ./app/assets/javascripts/discourse
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}" --random
timeout-minutes: 20
- name: Core QUnit 3
if: ${{ always() }}
working-directory: ./app/assets/javascripts/discourse
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}" --random
timeout-minutes: 20
with:
name: ember-exam-execution-${{matrix.browser}}
path: ./app/assets/javascripts/discourse/test-execution-*.json

View File

@ -17,6 +17,7 @@ lib/javascripts/messageformat.js
lib/highlight_js/
plugins/**/lib/javascripts/locale
public/
!/app/assets/javascripts/discourse/public
vendor/
app/assets/javascripts/discourse/tests/fixtures
spec/

View File

@ -149,11 +149,14 @@ group :assets do
end
group :test do
gem 'capybara', require: false
gem 'webmock', require: false
gem 'fakeweb', require: false
gem 'minitest', require: false
gem 'simplecov', require: false
gem 'selenium-webdriver', require: false
gem "test-prof"
gem 'webdrivers', require: false
end
group :test, :development do
@ -170,7 +173,7 @@ group :test, :development do
gem 'shoulda-matchers', require: false
gem 'rspec-html-matchers'
gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri
gem 'rubocop-discourse', require: false, github: 'discourse/rubocop-discourse'
gem 'rubocop-discourse', require: false
gem 'parallel_tests'
gem 'rswag-specs'

View File

@ -5,14 +5,6 @@ GIT
mail (2.8.0.edge)
mini_mime (>= 0.1.1)
GIT
remote: https://github.com/discourse/rubocop-discourse.git
revision: a5aea6e5f150b1eb7765a805bec0ff618cb718b3
specs:
rubocop-discourse (2.5.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
GEM
remote: https://rubygems.org/
specs:
@ -56,8 +48,8 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
@ -93,17 +85,27 @@ GEM
bootsnap (1.13.0)
msgpack (~> 1.2)
builder (3.2.4)
bullet (7.0.2)
bullet (7.0.3)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.3)
capybara (3.37.1)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
cbor (0.5.9.6)
certified (1.0.0)
childprocess (4.1.0)
chunky_png (1.4.0)
coderay (1.1.3)
colored2 (3.1.2)
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
connection_pool (2.3.0)
cose (1.2.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -111,12 +113,11 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.11.0)
css_parser (1.12.0)
addressable
debug_inspector (1.1.0)
diff-lcs (1.5.0)
diffy (3.4.2)
digest (3.1.0)
discourse-ember-rails (0.18.6)
active_model_serializers
ember-data-source (>= 1.0.0.beta.5)
@ -140,36 +141,17 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.11.0)
excon (0.92.4)
excon (0.92.5)
execjs (2.8.1)
exifr (1.3.9)
fabrication (2.30.0)
faker (2.22.0)
faker (2.23.0)
i18n (>= 1.8.11, < 2)
fakeweb (1.3.0)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
faraday (2.5.2)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday-net_http (3.0.0)
fast_blank (1.0.1)
fast_xs (0.8.0)
fastimage (2.2.6)
@ -194,7 +176,7 @@ GEM
image_size (>= 1.5, < 4)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
image_size (3.0.2)
image_size (3.1.0)
in_threads (1.6.0)
jmespath (1.6.1)
jquery-rails (4.5.0)
@ -209,7 +191,7 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.4.1)
jwt (2.5.0)
kgio (2.11.4)
libv8-node (16.10.0.0)
libv8-node (16.10.0.0-aarch64-linux)
@ -229,12 +211,13 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.11.2)
loofah (2.18.0)
logster (2.11.3)
loofah (2.19.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
lz4-ruby (0.3.3)
matrix (0.4.2)
maxminddb (0.1.22)
memory_profiler (1.0.0)
message_bus (4.2.0)
@ -242,36 +225,29 @@ GEM
method_source (1.0.0)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
mini_racer (0.6.2)
mini_racer (0.6.3)
libv8-node (~> 16.10.0.0)
mini_scheduler (0.14.0)
sidekiq (>= 4.2.3)
mini_sql (1.4.0)
mini_suffix (0.3.3)
ffi (~> 1.9)
minitest (5.16.2)
mocha (1.14.0)
msgpack (1.5.4)
minitest (5.16.3)
mocha (1.15.0)
msgpack (1.5.6)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.2.3)
mustache (1.1.1)
net-http (0.2.2)
uri
net-imap (0.2.3)
digest
net-imap (0.3.0)
net-protocol
strscan
net-pop (0.1.1)
digest
net-pop (0.1.2)
net-protocol
timeout
net-protocol (0.1.3)
timeout
net-smtp (0.3.1)
digest
net-smtp (0.3.2)
net-protocol
timeout
nio4r (2.5.8)
nokogiri (1.13.8)
mini_portile2 (~> 2.8.0)
@ -284,15 +260,20 @@ GEM
racc (~> 1.4)
nokogiri (1.13.8-x86_64-linux)
racc (~> 1.4)
oauth (0.5.10)
oauth2 (1.4.7)
faraday (>= 0.8, < 2.0)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
oauth-tty (1.0.3)
version_gem (~> 1.1)
oauth2 (1.4.11)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
rack (>= 1.2, < 4)
oj (3.13.14)
omniauth (1.9.1)
omniauth (1.9.2)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-facebook (9.0.0)
@ -308,40 +289,40 @@ GEM
omniauth-oauth (1.2.0)
oauth
omniauth (>= 1.0, < 3)
omniauth-oauth2 (1.7.2)
oauth2 (~> 1.4)
omniauth-oauth2 (1.7.3)
oauth2 (>= 1.4, < 3)
omniauth (>= 1.9, < 3)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
openssl (3.0.0)
openssl (3.0.1)
openssl-signature_algorithm (1.2.1)
openssl (> 2.0, < 3.1)
optimist (3.0.1)
parallel (1.22.1)
parallel_tests (3.11.1)
parallel_tests (3.13.0)
parallel
parser (3.1.2.1)
ast (~> 2.4.1)
pg (1.4.3)
progress (3.6.0)
pry (0.13.1)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.9.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (~> 0.13.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.7)
puma (5.6.4)
public_suffix (5.0.0)
puma (5.6.5)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.6.0)
rack (2.2.4)
rack-mini-profiler (3.0.0)
rack (>= 1.2.0)
rack-protection (2.2.2)
rack-protection (3.0.1)
rack
rack-test (2.0.2)
rack (>= 1.3)
@ -367,7 +348,7 @@ GEM
rainbow (3.1.1)
raindrops (0.20.0)
rake (13.0.6)
rb-fsevent (0.11.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtrace (0.4.14)
@ -376,9 +357,9 @@ GEM
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.7.1)
redis-namespace (1.8.2)
redis (>= 3.0.4)
regexp_parser (2.5.0)
redis-namespace (1.9.0)
redis (>= 4)
regexp_parser (2.6.0)
request_store (1.5.1)
rack (>= 1.4)
rexml (3.2.5)
@ -394,7 +375,7 @@ GEM
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.0)
rspec-expectations (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-html-matchers (0.10.0)
@ -411,27 +392,30 @@ GEM
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.11.0)
rspec-support (3.11.1)
rss (0.2.9)
rexml
rswag-specs (2.5.1)
rswag-specs (2.6.0)
activesupport (>= 3.1, < 7.1)
json-schema (~> 2.2)
railties (>= 3.1, < 7.1)
rubocop (1.34.1)
rubocop (1.36.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.20.0, < 2.0)
rubocop-ast (>= 1.20.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.21.0)
parser (>= 3.1.1.0)
rubocop-rspec (2.12.1)
rubocop (~> 1.31)
rubocop-discourse (3.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.13.2)
rubocop (~> 1.33)
ruby-prof (1.4.3)
ruby-progressbar (1.11.0)
ruby-readability (0.7.0)
@ -454,18 +438,26 @@ GEM
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
shoulda-matchers (5.1.0)
selenium-webdriver (4.5.0)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
shoulda-matchers (5.2.0)
activesupport (>= 5.2.0)
sidekiq (6.5.4)
connection_pool (>= 2.2.2)
sidekiq (6.5.7)
connection_pool (>= 2.2.5)
rack (~> 2.0)
redis (>= 4.5.0)
redis (>= 4.5.0, < 5)
simplecov (0.21.2)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
snaky_hash (2.0.0)
hashie
version_gem (~> 1.1)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -474,9 +466,8 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.20)
strscan (3.0.4)
test-prof (1.0.9)
stackprof (0.2.21)
test-prof (1.0.10)
thor (1.2.1)
tilt (2.0.11)
timeout (0.3.0)
@ -487,21 +478,29 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.2.0)
unicode-display_width (2.3.0)
unicorn (6.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.16.0)
uri (0.11.0)
uri_template (0.7.0)
webmock (3.17.1)
version_gem (1.1.0)
webdrivers (5.1.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (~> 4.0)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpush (1.1.0)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket (1.2.9)
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
yaml-lint (0.0.10)
zeitwerk (2.6.0)
@ -533,6 +532,7 @@ DEPENDENCIES
bootsnap
bullet
byebug
capybara
cbor
certified
colored2
@ -617,7 +617,7 @@ DEPENDENCIES
rspec-rails
rss
rswag-specs
rubocop-discourse!
rubocop-discourse
ruby-prof
ruby-readability
rubyzip
@ -625,6 +625,7 @@ DEPENDENCIES
sassc (= 2.0.1)
sassc-rails
seed-fu
selenium-webdriver
shoulda-matchers
sidekiq
simplecov
@ -637,6 +638,7 @@ DEPENDENCIES
uglifier
unf
unicorn
webdrivers
webmock
webpush
xorcist

View File

@ -12,11 +12,9 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
## Screenshots
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://user-images.githubusercontent.com/1681963/52239245-04ad8280-289c-11e9-9c88-8c173d4a0422.png" width="720px"></a>
<a href="https://twittercommunity.com/"><img src="https://user-images.githubusercontent.com/1681963/52239250-04ad8280-289c-11e9-9e42-574f6eaab9d7.png" width="720px"></a>
<a href="https://forums.gearboxsoftware.com/"><img src="https://user-images.githubusercontent.com/1681963/89088042-68ffb400-d364-11ea-93be-161ea04d8b29.png" width="720px"></a>
<img src="https://user-images.githubusercontent.com/1681963/52239118-b304f800-289b-11e9-9904-16450680d9ec.jpg" alt="Mobile" width="414">
@ -111,7 +109,7 @@ Discourse logo and “Discourse Forum” ®, Civilized Discourse Construction Ki
## Accessibility
To guide our ongoing effort to build accessible software we follow the [W3Cs Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/TR/WCAG21/). If you'd like to report an accessibility issue that makes it difficult for you to use Discourse, email accessibility@discourse.org. For more information visit [discourse.org/accessibility](https://discourse.org/accessibility).
To guide our ongoing effort to build accessible software we follow the [W3Cs Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/TR/WCAG21/). If you'd like to report an accessibility issue that makes it difficult for you to use Discourse, email accessibility@discourse.org. For more information visit [discourse.org/accessibility](https://discourse.org/accessibility).
## Dedication

View File

@ -1,11 +0,0 @@
<%
DiscoursePluginRegistry.admin_javascripts.each { |js| require_asset(js) }
DiscoursePluginRegistry.each_globbed_asset(admin: true) do |f|
if File.directory?(f)
depend_on(f)
else
require_asset(f)
end
end
%>

View File

@ -1,12 +1,13 @@
import Component from "@ember/component";
import { alias, equal } from "@ember/object/computed";
import bootbox from "bootbox";
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(),
isReplace: equal("actionKey", "replace"),
isTag: equal("actionKey", "tag"),
@ -26,7 +27,7 @@ export default Component.extend({
this.action(this.word);
})
.catch((e) => {
bootbox.alert(
this.dialog.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})

View File

@ -2,15 +2,16 @@ import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import Component from "@ember/component";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
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) {
@ -52,25 +53,21 @@ export default Component.extend({
actions: {
redeliver() {
return bootbox.confirm(
I18n.t("admin.web_hooks.events.redeliver_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then((json) => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
}
}
);
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() {

View File

@ -1,10 +1,11 @@
import Component from "@ember/component";
import I18n from "I18n";
import bootbox from "bootbox";
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"),
@discourseComputed("fieldName")
@ -33,22 +34,18 @@ export default Component.extend({
actions: {
reset() {
bootbox.confirm(
I18n.t("admin.customize.email_style.reset_confirm", {
this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.email_style.reset_confirm", {
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.styles.setField(
this.fieldName,
this.styles.get(`default_${this.fieldName}`)
);
this.notifyPropertyChange("editorContents");
}
}
);
didConfirm: () => {
this.styles.setField(
this.fieldName,
this.styles.get(`default_${this.fieldName}`)
);
this.notifyPropertyChange("editorContents");
},
});
},
save() {
this.attrs.save();

View File

@ -3,13 +3,15 @@ import Component from "@ember/component";
import EmberObject from "@ember/object";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
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(),
@discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
@ -89,29 +91,27 @@ export default Component.extend({
},
deleteOtherAccounts() {
bootbox.confirm(
I18n.t("ip_lookup.confirm_delete_other_accounts"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.setProperties({
other_accounts: null,
otherAccountsLoading: true,
totalOthersWithSameIP: null,
});
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",
},
}).then(() => this.send("lookup"));
}
}
);
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,14 +1,15 @@
import Component from "@ember/component";
import I18n from "I18n";
import Permalink from "admin/models/permalink";
import bootbox from "bootbox";
import discourseComputed, { bind } from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import { schedule } from "@ember/runloop";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default Component.extend({
tagName: "",
dialog: service(),
formSubmitted: false,
permalinkType: "topic_id",
permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),
@ -29,7 +30,7 @@ export default Component.extend({
@bind
focusPermalink() {
schedule("afterRender", () =>
this.element.querySelector(".permalink-url")?.focus()
document.querySelector(".permalink-url")?.focus()
);
},
@ -74,7 +75,12 @@ export default Component.extend({
} else {
error = I18n.t("generic_error");
}
bootbox.alert(error, this.focusPermalink);
this.dialog.alert({
message: error,
didConfirm: () => this.focusPermalink(),
didCancel: () => this.focusPermalink(),
});
}
);
}

View File

@ -2,8 +2,8 @@ import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import ScreenedIpAddress from "admin/models/screened-ip-address";
import bootbox from "bootbox";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
/**
A form to create an IP address that will be blocked or allowed.
@ -18,6 +18,7 @@ import { schedule } from "@ember/runloop";
export default Component.extend({
tagName: "form",
dialog: service(),
classNames: ["screened-ip-address-form", "inline-form"],
formSubmitted: false,
actionName: "block",
@ -47,6 +48,12 @@ export default Component.extend({
}
},
focusInput() {
schedule("afterRender", () => {
this.element.querySelector("input").focus();
});
},
actions: {
submit() {
if (!this.formSubmitted) {
@ -60,22 +67,20 @@ export default Component.extend({
.then((result) => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
schedule("afterRender", () =>
this.element.querySelector("input").focus()
);
this.focusInput();
})
.catch((e) => {
this.set("formSubmitted", false);
const msg = e.jqXHR.responseJSON?.errors
const message = e.jqXHR.responseJSON?.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
schedule("afterRender", () =>
this.element.querySelector("input").focus()
)
);
this.dialog.alert({
message,
didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(),
});
});
}
},

View File

@ -2,10 +2,11 @@ import Component from "@ember/component";
import I18n from "I18n";
import UppyUploadMixin from "discourse/mixins/uppy-upload";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import { inject as service } from "@ember/service";
export default Component.extend(UppyUploadMixin, {
type: "csv",
dialog: service(),
uploadUrl: "/tags/upload",
addDisabled: alias("uploading"),
elementId: "tag-uploader",
@ -16,9 +17,8 @@ export default Component.extend(UppyUploadMixin, {
},
uploadDone() {
bootbox.alert(I18n.t("tagging.upload_successful"), () => {
this.refresh();
this.closeModal();
});
this.closeModal();
this.refresh();
this.dialog.alert(I18n.t("tagging.upload_successful"));
},
});

View File

@ -2,13 +2,14 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import WatchedWord from "admin/models/watched-word";
import bootbox from "bootbox";
import { equal } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
export default Component.extend({
tagName: "form",
dialog: service(),
classNames: ["watched-word-form"],
formSubmitted: false,
actionKey: null,
@ -55,6 +56,10 @@ export default Component.extend({
});
},
focusInput() {
schedule("afterRender", () => this.element.querySelector("input").focus());
},
actions: {
changeSelectedTags(tags) {
this.setProperties({
@ -98,22 +103,20 @@ export default Component.extend({
isCaseSensitive: false,
});
this.action(WatchedWord.create(result));
schedule("afterRender", () =>
this.element.querySelector("input").focus()
);
this.focusInput();
})
.catch((e) => {
this.set("formSubmitted", false);
const msg = e.jqXHR.responseJSON?.errors
const message = e.jqXHR.responseJSON?.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
schedule("afterRender", () =>
this.element.querySelector("input").focus()
)
);
this.dialog.alert({
message,
didConfirm: () => this.focusInput(),
didCancel: () => this.focusInput(),
});
});
}
},

View File

@ -2,7 +2,7 @@ import Component from "@ember/component";
import I18n from "I18n";
import UppyUploadMixin from "discourse/mixins/uppy-upload";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import { dialog } from "discourse/lib/uploads";
export default Component.extend(UppyUploadMixin, {
type: "txt",
@ -21,7 +21,7 @@ export default Component.extend(UppyUploadMixin, {
uploadDone() {
if (this) {
bootbox.alert(I18n.t("admin.watched_words.form.upload_successful"));
dialog.alert(I18n.t("admin.watched_words.form.upload_successful"));
this.done();
}
},

View File

@ -3,11 +3,13 @@ import { alias, equal } from "@ember/object/computed";
import { i18n, setting } from "discourse/lib/computed";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminBackups: controller(),
dialog: service(),
status: alias("adminBackups.model"),
uploadLabel: i18n("admin.backups.upload.label"),
backupLocation: setting("backup_location"),
@ -27,17 +29,13 @@ export default Controller.extend({
actions: {
toggleReadOnlyMode() {
if (!this.site.get("isReadOnly")) {
bootbox.confirm(
I18n.t("admin.backups.read_only.enable.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.set("currentUser.hideReadOnlyAlert", true);
this._toggleReadOnlyMode(true);
}
}
);
this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.read_only.enable.confirm"),
didConfirm: () => {
this.set("currentUser.hideReadOnlyAlert", true);
this._toggleReadOnlyMode(true);
},
});
} else {
this._toggleReadOnlyMode(false);
}
@ -46,7 +44,7 @@ export default Controller.extend({
download(backup) {
const link = backup.get("filename");
ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() =>
bootbox.alert(I18n.t("admin.backups.operations.download.alert"))
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"))
);
},
},

View File

@ -1,12 +1,13 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { extractError } from "discourse/lib/ajax-error";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
export default class AdminBadgesAwardController extends Controller {
@service dialog;
@tracked saving = false;
@tracked replaceBadgeOwners = false;
@tracked grantExistingHolders = false;
@ -84,7 +85,7 @@ export default class AdminBadgesAwardController extends Controller {
})
.finally(() => (this.saving = false));
} else {
bootbox.alert(I18n.t("admin.badges.mass_award.aborted"));
this.dialog.alert(I18n.t("admin.badges.mass_award.aborted"));
}
}
}

View File

@ -1,7 +1,7 @@
import Controller, { inject as controller } from "@ember/controller";
import { observes } from "discourse-common/utils/decorators";
import I18n from "I18n";
import bootbox from "bootbox";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { next } from "@ember/runloop";
@ -17,8 +17,9 @@ const ICON = "icon";
export default class AdminBadgesShowController extends Controller.extend(
bufferedProperty("model")
) {
@controller adminBadges;
@service router;
@service dialog;
@controller adminBadges;
@tracked saving = false;
@tracked savingStatus = "";
@ -81,8 +82,8 @@ export default class AdminBadgesShowController extends Controller.extend(
}
get hasQuery() {
let modelQuery = this.model.query;
let bufferedQuery = this.bufferedQuery;
let modelQuery = this.model.get("query");
let bufferedQuery = this.buffered.get("query");
if (bufferedQuery) {
return bufferedQuery.trim().length > 0;
@ -216,23 +217,19 @@ export default class AdminBadgesShowController extends Controller.extend(
return;
}
return bootbox.confirm(
I18n.t("admin.badges.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
model
.destroy()
.then(() => {
adminBadges.removeObject(model);
this.transitionToRoute("adminBadges.index");
})
.catch(() => {
bootbox.alert(I18n.t("generic_error"));
});
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.badges.delete_confirm"),
didConfirm: () => {
model
.destroy()
.then(() => {
adminBadges.removeObject(model);
this.transitionToRoute("adminBadges.index");
})
.catch(() => {
this.dialog.alert(I18n.t("generic_error"));
});
},
});
}
}

View File

@ -1,11 +1,12 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import bootbox from "bootbox";
import discourseLater from "discourse-common/lib/later";
import { action, computed } from "@ember/object";
import { clipboardCopy } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
export default class AdminCustomizeColorsShowController extends Controller {
@service dialog;
onlyOverridden = false;
@computed("model.colors.[]", "onlyOverridden")
@ -73,18 +74,14 @@ export default class AdminCustomizeColorsShowController extends Controller {
@action
destroy() {
return bootbox.confirm(
I18n.t("admin.customize.colors.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.model.destroy().then(() => {
this.allColors.removeObject(this.model);
this.replaceRoute("adminCustomize.colors");
});
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.colors.delete_confirm"),
didConfirm: () => {
return this.model.destroy().then(() => {
this.allColors.removeObject(this.model);
this.replaceRoute("adminCustomize.colors");
});
},
});
}
}

View File

@ -1,9 +1,11 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
@ -27,7 +29,7 @@ export default Controller.extend({
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
bootbox.alert(msg);
this.dialog.alert(msg);
})
.finally(() => this.set("model.changed", false));
}

View File

@ -16,10 +16,12 @@ import { makeArray } from "discourse-common/lib/helpers";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import { url } from "discourse/lib/computed";
import { inject as service } from "@ember/service";
const THEME_UPLOAD_VAR = 2;
export default Controller.extend({
dialog: service(),
downloadUrl: url("model.id", "/admin/customize/themes/%@/export"),
previewUrl: url("model.id", "/admin/themes/%@/preview"),
addButtonDisabled: empty("selectedChildThemeId"),
@ -345,16 +347,10 @@ export default Controller.extend({
},
removeUpload(upload) {
return bootbox.confirm(
I18n.t("admin.customize.theme.delete_upload_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.model.removeField(upload);
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.theme.delete_upload_confirm"),
didConfirm: () => this.model.removeField(upload),
});
},
removeChildTheme(theme) {
@ -364,23 +360,19 @@ export default Controller.extend({
},
destroy() {
return bootbox.confirm(
I18n.t("admin.customize.delete_confirm", {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.delete_confirm", {
theme_name: this.get("model.name"),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
const model = this.model;
model.setProperties({ recentlyInstalled: false });
model.destroyRecord().then(() => {
this.allThemes.removeObject(model);
this.transitionToRoute("adminCustomizeThemes");
});
}
}
);
didConfirm: () => {
const model = this.model;
model.setProperties({ recentlyInstalled: false });
model.destroyRecord().then(() => {
this.allThemes.removeObject(model);
this.transitionToRoute("adminCustomizeThemes");
});
},
});
},
switchType() {
@ -398,16 +390,10 @@ export default Controller.extend({
});
}
bootbox.confirm(
return this.dialog.yesNoConfirm({
message,
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.commitSwitchType();
}
}
);
didConfirm: () => this.commitSwitchType(),
});
},
enableComponent() {

View File

@ -1,11 +1,15 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { empty } from "@ember/object/computed";
import { observes } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { escapeExpression } from "discourse/lib/utilities";
export default Controller.extend({
dialog: service(),
/**
Is the "send test email" button disabled?
@ -44,13 +48,17 @@ export default Controller.extend({
)
.catch((e) => {
if (e.jqXHR.responseJSON?.errors) {
bootbox.alert(
I18n.t("admin.email.error", {
server_error: e.jqXHR.responseJSON.errors[0],
})
);
this.dialog.alert({
message: htmlSafe(
I18n.t("admin.email.error", {
server_error: escapeExpression(
e.jqXHR.responseJSON.errors[0]
),
})
),
});
} else {
bootbox.alert(I18n.t("admin.email.test_error"));
this.dialog.alert({ message: I18n.t("admin.email.test_error") });
}
})
.finally(() => this.set("sendingEmail", false));

View File

@ -1,14 +1,14 @@
import { empty, notEmpty, or } from "@ember/object/computed";
import Controller from "@ember/controller";
import EmailPreview from "admin/models/email-preview";
import bootbox from "bootbox";
import { get } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
username: null,
lastSeen: null,
emailEmpty: empty("email"),
sendEmailDisabled: or("emailEmpty", "sendingEmail"),
showSendEmailForm: notEmpty("model.html_content"),
@ -50,7 +50,7 @@ export default Controller.extend({
EmailPreview.sendDigest(this.username, this.lastSeen, this.email)
.then((result) => {
if (result.errors) {
bootbox.alert(result.errors);
this.dialog.alert(result.errors);
} else {
this.set("sentEmail", true);
}

View File

@ -2,12 +2,13 @@ import EmberObject, { action, computed } from "@ember/object";
import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { sort } from "@ember/object/computed";
import { inject as service } from "@ember/service";
const ALL_FILTER = "all";
export default Controller.extend({
dialog: service(),
filter: null,
sorting: null,
@ -72,19 +73,17 @@ export default Controller.extend({
@action
destroyEmoji(emoji) {
return bootbox.confirm(
I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
I18n.t("no_value"),
I18n.t("yes_value"),
(destroy) => {
if (destroy) {
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE",
}).then(() => {
this.model.removeObject(emoji);
});
}
}
);
this.dialog.yesNoConfirm({
message: I18n.t("admin.emoji.delete_confirm", {
name: emoji.get("name"),
}),
didConfirm: () => {
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
type: "DELETE",
}).then(() => {
this.model.removeObject(emoji);
});
},
});
},
});

View File

@ -2,13 +2,14 @@ import Controller from "@ember/controller";
import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment";
import ScreenedIpAddress from "admin/models/screened-ip-address";
import bootbox from "bootbox";
import discourseDebounce from "discourse-common/lib/debounce";
import { exportEntity } from "discourse/lib/export-csv";
import { observes } from "discourse-common/utils/decorators";
import { outputExportResult } from "discourse/lib/export-result";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
loading: false,
filter: null,
savedIpAddress: null,
@ -59,13 +60,13 @@ export default Controller.extend({
.then(() => this.set("savedIpAddress", null))
.catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
bootbox.alert(
this.dialog.alert(
I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
);
} else {
bootbox.alert(I18n.t("generic_error"));
this.dialog.alert(I18n.t("generic_error"));
}
if (wasEditing) {
record.set("editing", true);
@ -74,33 +75,29 @@ export default Controller.extend({
},
destroy(record) {
return bootbox.confirm(
I18n.t("admin.logs.screened_ips.delete_confirm", {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.logs.screened_ips.delete_confirm", {
ip_address: record.get("ip_address"),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
record
.destroy()
.then((deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
bootbox.alert(I18n.t("generic_error"));
}
})
.catch((e) => {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})
);
});
}
}
);
didConfirm: () => {
return record
.destroy()
.then((deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
})
.catch((e) => {
this.dialog.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})
);
});
},
});
},
recordAdded(arg) {

View File

@ -2,12 +2,13 @@ import Controller from "@ember/controller";
import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment";
import Permalink from "admin/models/permalink";
import bootbox from "bootbox";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { clipboardCopy } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
loading: false,
filter: null,
@ -34,27 +35,23 @@ export default Controller.extend({
},
destroy(record) {
return bootbox.confirm(
I18n.t("admin.permalink.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
record.destroy().then(
(deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
bootbox.alert(I18n.t("generic_error"));
}
},
function () {
bootbox.alert(I18n.t("generic_error"));
return this.dialog.yesNoConfirm({
message: I18n.t("admin.permalink.delete_confirm"),
didConfirm: () => {
return record.destroy().then(
(deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
);
}
}
);
},
function () {
this.dialog.alert(I18n.t("generic_error"));
}
);
},
});
},
},
});

View File

@ -1,11 +1,13 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import bootbox from "bootbox";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default Controller.extend(bufferedProperty("siteText"), {
dialog: service(),
saved: false,
queryParams: ["locale"],
@ -14,35 +16,36 @@ export default Controller.extend(bufferedProperty("siteText"), {
return this.siteText.value === value;
},
actions: {
saveChanges() {
const attrs = this.buffered.getProperties("value");
attrs.locale = this.locale;
@action
saveChanges() {
const attrs = this.buffered.getProperties("value");
attrs.locale = this.locale;
this.siteText
.save(attrs)
.then(() => {
this.commitBuffer();
this.set("saved", true);
})
.catch(popupAjaxError);
},
this.siteText
.save(attrs)
.then(() => {
this.commitBuffer();
this.set("saved", true);
})
.catch(popupAjaxError);
},
revertChanges() {
this.set("saved", false);
@action
revertChanges() {
this.set("saved", false);
bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), (result) => {
if (result) {
this.siteText
.revert(this.locale)
.then((props) => {
const buffered = this.buffered;
buffered.setProperties(props);
this.commitBuffer();
})
.catch(popupAjaxError);
}
});
},
this.dialog.yesNoConfirm({
message: I18n.t("admin.site_text.revert_confirm"),
didConfirm: () => {
this.siteText
.revert(this.locale)
.then((props) => {
const buffered = this.buffered;
buffered.setProperties(props);
this.commitBuffer();
})
.catch(popupAjaxError);
},
});
},
});

View File

@ -42,21 +42,11 @@ export default Controller.extend({
}
},
@discourseComputed("locale")
showFallbackLocaleWarning() {
return (
this.siteSettings.allow_user_locale &&
this.siteSettings.set_locale_from_accept_language_header &&
this.fallbackLocaleFullName
);
},
actions: {
edit(siteText) {
this.transitionToRoute("adminSiteText.edit", siteText.get("id"), {
queryParams: {
locale: this.locale,
localeFullName: this.availableLocales[this.locale],
},
});
},

View File

@ -2,13 +2,14 @@ import Controller, { inject as controller } from "@ember/controller";
import { alias, sort } from "@ember/object/computed";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import I18n from "I18n";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { next } from "@ember/runloop";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend(GrantBadgeController, {
adminUser: controller(),
dialog: service(),
user: alias("adminUser.model"),
userBadges: alias("model"),
allBadges: alias("badges"),
@ -90,18 +91,14 @@ export default Controller.extend(GrantBadgeController, {
},
revokeBadge(userBadge) {
return bootbox.confirm(
I18n.t("admin.badges.revoke_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
userBadge.revoke().then(() => {
this.model.removeObject(userBadge);
});
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.badges.revoke_confirm"),
didConfirm: () => {
return userBadge.revoke().then(() => {
this.model.removeObject(userBadge);
});
},
});
},
},
});

View File

@ -6,17 +6,16 @@ import CanCheckEmails from "discourse/mixins/can-check-emails";
import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import { htmlSafe } from "@ember/template";
import { iconHTML } from "discourse-common/lib/icon-library";
import { extractError, popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import showModal from "discourse/lib/show-modal";
export default Controller.extend(CanCheckEmails, {
router: service(),
dialog: service(),
adminTools: service(),
originalPrimaryGroupId: null,
customGroupIdsBuffer: null,
@ -130,7 +129,7 @@ export default Controller.extend(CanCheckEmails, {
groupAdded(added) {
this.model
.groupAdded(added)
.catch(() => bootbox.alert(I18n.t("generic_error")));
.catch(() => this.dialog.alert(I18n.t("generic_error")));
},
groupRemoved(groupId) {
@ -141,7 +140,7 @@ export default Controller.extend(CanCheckEmails, {
this.set("originalPrimaryGroupId", null);
}
})
.catch(() => bootbox.alert(I18n.t("generic_error")));
.catch(() => this.dialog.alert(I18n.t("generic_error")));
},
@discourseComputed("ssoLastPayload")
@ -156,16 +155,16 @@ export default Controller.extend(CanCheckEmails, {
.then(() => DiscourseURL.redirectTo("/"))
.catch((e) => {
if (e.status === 404) {
bootbox.alert(I18n.t("admin.impersonate.not_found"));
this.dialog.alert(I18n.t("admin.impersonate.not_found"));
} else {
bootbox.alert(I18n.t("admin.impersonate.invalid"));
this.dialog.alert(I18n.t("admin.impersonate.invalid"));
}
});
},
logOut() {
return this.model
.logOut()
.then(() => bootbox.alert(I18n.t("admin.user.logged_out")));
.then(() => this.dialog.alert(I18n.t("admin.user.logged_out")));
},
resetBounceScore() {
return this.model.resetBounceScore();
@ -188,13 +187,15 @@ export default Controller.extend(CanCheckEmails, {
const error = I18n.t("admin.user.deactivate_failed", {
error: this._formatError(e),
});
bootbox.alert(error);
this.dialog.alert(error);
});
},
sendActivationEmail() {
return this.model
.sendActivationEmail()
.then(() => bootbox.alert(I18n.t("admin.user.activation_email_sent")))
.then(() =>
this.dialog.alert(I18n.t("admin.user.activation_email_sent"))
)
.catch(popupAjaxError);
},
activate() {
@ -210,7 +211,7 @@ export default Controller.extend(CanCheckEmails, {
const error = I18n.t("admin.user.activate_failed", {
error: this._formatError(e),
});
bootbox.alert(error);
this.dialog.alert(error);
});
},
revokeAdmin() {
@ -221,7 +222,7 @@ export default Controller.extend(CanCheckEmails, {
.grantAdmin()
.then((result) => {
if (result.email_confirmation_required) {
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
this.dialog.alert(I18n.t("admin.user.grant_admin_confirm"));
}
})
.catch((error) => {
@ -255,7 +256,7 @@ export default Controller.extend(CanCheckEmails, {
I18n.t("admin.user.trust_level_change_failed", {
error: this._formatError(e),
});
bootbox.alert(error);
this.dialog.alert(error);
});
},
restoreTrustLevel() {
@ -275,7 +276,7 @@ export default Controller.extend(CanCheckEmails, {
I18n.t("admin.user.trust_level_change_failed", {
error: this._formatError(e),
});
bootbox.alert(error);
this.dialog.alert(error);
});
},
unsilence() {
@ -287,7 +288,6 @@ export default Controller.extend(CanCheckEmails, {
anonymize() {
const user = this.model;
const message = I18n.t("admin.user.anonymize_confirm");
const performAnonymize = () => {
this.model
@ -302,31 +302,32 @@ export default Controller.extend(CanCheckEmails, {
document.location = getURL("/admin/users/list/active");
}
} else {
bootbox.alert(I18n.t("admin.user.anonymize_failed"));
this.dialog.alert(I18n.t("admin.user.anonymize_failed"));
if (data.user) {
user.setProperties(data.user);
}
}
})
.catch(() => bootbox.alert(I18n.t("admin.user.anonymize_failed")));
.catch(() =>
this.dialog.alert(I18n.t("admin.user.anonymize_failed"))
);
};
const buttons = [
{
label: I18n.t("composer.cancel"),
class: "cancel",
link: true,
},
{
label: I18n.t("admin.user.anonymize_yes"),
class: "btn btn-danger",
icon: iconHTML("exclamation-triangle"),
callback: () => {
performAnonymize();
},
},
];
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
this.dialog.alert({
message: I18n.t("admin.user.anonymize_confirm"),
class: "delete-user-modal",
buttons: [
{
icon: "exclamation-triangle",
label: I18n.t("admin.user.anonymize_yes"),
class: "btn-danger",
action: () => performAnonymize(),
},
{
label: I18n.t("composer.cancel"),
},
],
});
},
disableSecondFactor() {
@ -345,11 +346,10 @@ export default Controller.extend(CanCheckEmails, {
destroy() {
const postCount = this.get("model.post_count");
const maxPostCount = this.siteSettings.delete_all_posts_max;
const message = I18n.t("admin.user.delete_confirm");
const location = document.location.pathname;
const performDestroy = (block) => {
bootbox.dialog(I18n.t("admin.user.deleting_user"));
this.dialog.notice(I18n.t("admin.user.deleting_user"));
let formData = { context: location };
if (block) {
formData["block_email"] = true;
@ -369,38 +369,39 @@ export default Controller.extend(CanCheckEmails, {
document.location = getURL("/admin/users/list/active");
}
} else {
bootbox.alert(I18n.t("admin.user.delete_failed"));
this.dialog.alert(I18n.t("admin.user.delete_failed"));
}
})
.catch(() => {
bootbox.alert(I18n.t("admin.user.delete_failed"));
this.dialog.alert(I18n.t("admin.user.delete_failed"));
});
};
const buttons = [
{
label: I18n.t("composer.cancel"),
class: "btn",
link: true,
},
{
icon: iconHTML("exclamation-triangle"),
label: I18n.t("admin.user.delete_and_block"),
class: "btn btn-danger",
callback: () => {
performDestroy(true);
this.dialog.alert({
title: I18n.t("admin.user.delete_confirm_title"),
message: I18n.t("admin.user.delete_confirm"),
class: "delete-user-modal",
buttons: [
{
label: I18n.t("admin.user.delete_dont_block"),
class: "btn-primary",
action: () => {
return performDestroy(false);
},
},
},
{
label: I18n.t("admin.user.delete_dont_block"),
class: "btn btn-primary",
callback: () => {
performDestroy(false);
{
icon: "exclamation-triangle",
label: I18n.t("admin.user.delete_and_block"),
class: "btn-danger",
action: () => {
return performDestroy(true);
},
},
},
];
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
{
label: I18n.t("composer.cancel"),
},
],
});
},
promptTargetUser() {
@ -439,12 +440,12 @@ export default Controller.extend(CanCheckEmails, {
model: this.model,
});
} else {
bootbox.alert(I18n.t("admin.user.merge_failed"));
this.dialog.alert(I18n.t("admin.user.merge_failed"));
}
})
.catch(() => {
AdminUser.find(user.id).then((u) => user.setProperties(u));
bootbox.alert(I18n.t("admin.user.merge_failed"));
this.dialog.alert(I18n.t("admin.user.merge_failed"));
});
},
@ -532,7 +533,7 @@ export default Controller.extend(CanCheckEmails, {
data: { primary_group_id: primaryGroupId },
})
.then(() => this.set("originalPrimaryGroupId", primaryGroupId))
.catch(() => bootbox.alert(I18n.t("generic_error")));
.catch(() => this.dialog.alert(I18n.t("generic_error")));
},
resetPrimaryGroup() {
@ -540,16 +541,10 @@ export default Controller.extend(CanCheckEmails, {
},
deleteSSORecord() {
return bootbox.confirm(
I18n.t("admin.user.discourse_connect.confirm_delete"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
return this.model.deleteSSORecord();
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.user.discourse_connect.confirm_delete"),
didConfirm: () => this.model.deleteSSORecord(),
});
},
checkSsoEmail() {
@ -607,7 +602,7 @@ export default Controller.extend(CanCheckEmails, {
let error;
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
error = extractError(e) || I18n.t("admin.user.delete_posts_failed");
bootbox.alert(error);
this.dialog.alert(error);
});
};

View File

@ -59,10 +59,10 @@ export default Controller.extend(CanCheckEmails, {
page,
})
.then((result) => {
if (result && result.length > 0) {
this._results[page] = result;
this.set("model", this._results.flat());
} else {
this._results[page] = result;
this.set("model", this._results.flat());
if (result.length === 0) {
this._canLoadMore = false;
}
})

View File

@ -2,16 +2,17 @@ import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import WatchedWord from "admin/models/watched-word";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWatchedWords: controller(),
actionNameKey: null,
dialog: service(),
downloadLink: fmt(
"actionNameKey",
"/admin/customize/watched_words/action/%@/download"
@ -93,25 +94,21 @@ export default Controller.extend({
clearAll() {
const actionKey = this.actionNameKey;
bootbox.confirm(
I18n.t("admin.watched_words.clear_all_confirm", {
this.dialog.yesNoConfirm({
message: I18n.t("admin.watched_words.clear_all_confirm", {
action: I18n.t("admin.watched_words.actions." + actionKey),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
ajax(`/admin/customize/watched_words/action/${actionKey}.json`, {
type: "DELETE",
}).then(() => {
const action = this.findAction(actionKey);
if (action) {
action.set("words", []);
}
});
}
}
);
didConfirm: () => {
ajax(`/admin/customize/watched_words/action/${actionKey}.json`, {
type: "DELETE",
}).then(() => {
const action = this.findAction(actionKey);
if (action) {
action.set("words", []);
}
});
},
});
},
},
});

View File

@ -2,15 +2,16 @@ import Controller, { inject as controller } from "@ember/controller";
import EmberObject from "@ember/object";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { extractDomainFromUrl } from "discourse/lib/utilities";
import { isAbsoluteURL } from "discourse-common/lib/get-url";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
eventTypes: alias("adminWebHooks.eventTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
contentTypes: alias("adminWebHooks.contentTypes"),
@ -113,39 +114,28 @@ export default Controller.extend({
domain.match(/127\.\d+\.\d+\.\d+/) ||
isAbsoluteURL(url)
) {
return bootbox.confirm(
I18n.t("admin.web_hooks.warn_local_payload_url"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
return saveWebHook();
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.warn_local_payload_url"),
didConfirm: () => saveWebHook(),
});
}
return saveWebHook();
},
destroy() {
return bootbox.confirm(
I18n.t("admin.web_hooks.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
const model = this.model;
model
.destroyRecord()
.then(() => {
this.adminWebHooks.get("model").removeObject(model);
this.transitionToRoute("adminWebHooks");
})
.catch(popupAjaxError);
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.delete_confirm"),
didConfirm: () => {
this.model
.destroyRecord()
.then(() => {
this.adminWebHooks.get("model").removeObject(this.model);
this.transitionToRoute("adminWebHooks");
})
.catch(popupAjaxError);
},
});
},
},
});

View File

@ -1,30 +1,29 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import bootbox from "bootbox";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default Controller.extend({
actions: {
destroy(webhook) {
return bootbox.confirm(
I18n.t("admin.web_hooks.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
webhook
.destroyRecord()
.then(() => {
this.model.removeObject(webhook);
})
.catch(popupAjaxError);
}
}
);
},
dialog: service(),
loadMore() {
this.model.loadMore();
},
@action
destroy(webhook) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.delete_confirm"),
didConfirm: () => {
webhook
.destroyRecord()
.then(() => {
this.model.removeObject(webhook);
})
.catch(popupAjaxError);
},
});
},
@action
loadMore() {
this.model.loadMore();
},
});

View File

@ -3,10 +3,12 @@ import Controller from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { observes } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend(ModalFunctionality, {
dialog: service(),
@observes("model")
modelChanged() {
const model = this.model;
@ -78,7 +80,7 @@ export default Controller.extend(ModalFunctionality, {
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
() => bootbox.alert(I18n.t("generic_error"))
() => this.dialog.alert(I18n.t("generic_error"))
);
},
},

View File

@ -24,7 +24,7 @@ export default Controller.extend(ModalFunctionality, {
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
recordType: "theme",
checkPrivate: match("uploadUrl", /^ssh\:\/\/.*\@.*\.git$|.*\@.*\:.*\.git$/),
checkPrivate: match("uploadUrl", /^ssh:\/\/.+@.+$|.+@.+:.+$/),
localFile: null,
uploadUrl: null,
uploadName: null,
@ -93,10 +93,7 @@ export default Controller.extend(ModalFunctionality, {
this._keyLoading = true;
ajax(this.keyGenUrl, { type: "POST" })
.then((pair) => {
this.setProperties({
privateKey: pair.private_key,
publicKey: pair.public_key,
});
this.set("publicKey", pair.public_key);
})
.catch(popupAjaxError)
.finally(() => {
@ -139,7 +136,6 @@ export default Controller.extend(ModalFunctionality, {
this.setProperties({
duplicateRemoteThemeWarning: null,
privateChecked: false,
privateKey: null,
localFile: null,
uploadUrl: null,
publicKey: null,
@ -216,7 +212,7 @@ export default Controller.extend(ModalFunctionality, {
};
if (this.privateChecked) {
options.data.private_key = this.privateKey;
options.data.public_key = this.publicKey;
}
}
@ -239,10 +235,10 @@ export default Controller.extend(ModalFunctionality, {
this.send("closeModal");
})
.then(() => {
this.setProperties({ privateKey: null, publicKey: null });
this.set("publicKey", null);
})
.catch((error) => {
if (!this.privateKey || this.themeCannotBeInstalled) {
if (!this.publicKey || this.themeCannotBeInstalled) {
return popupAjaxError(error);
}

View File

@ -2,9 +2,10 @@ import Controller from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { inject as service } from "@ember/service";
export default Controller.extend(ModalFunctionality, {
dialog: service(),
loading: true,
reseeding: false,
categories: null,
@ -35,11 +36,11 @@ export default Controller.extend(ModalFunctionality, {
},
type: "POST",
})
.then(
() => this.send("closeModal"),
() => bootbox.alert(I18n.t("generic_error"))
)
.finally(() => this.set("reseeding", false));
.catch(() => this.dialog.alert(I18n.t("generic_error")))
.finally(() => {
this.set("reseeding", false);
this.send("closeModal");
});
},
},
});

View File

@ -6,14 +6,15 @@ import I18n from "I18n";
import PreloadStore from "discourse/lib/preload-store";
import User from "discourse/models/user";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { extractError } from "discourse/lib/ajax-error";
import getURL from "discourse-common/lib/get-url";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
const LOG_CHANNEL = "/admin/backups/logs";
export default DiscourseRoute.extend({
dialog: service(),
activate() {
this.messageBus.subscribe(LOG_CHANNEL, (log) => {
if (log.message === "[STARTED]") {
@ -28,7 +29,7 @@ export default DiscourseRoute.extend({
"model.isOperationRunning",
false
);
bootbox.alert(
this.dialog.alert(
I18n.t("admin.backups.operations.failed", {
operation: log.operation,
})
@ -77,88 +78,72 @@ export default DiscourseRoute.extend({
this.transitionTo("admin.backups.logs");
Backup.start(withUploads).then((result) => {
if (!result.success) {
bootbox.alert(result.message);
this.dialog.alert(result.message);
}
});
},
destroyBackup(backup) {
bootbox.confirm(
I18n.t("admin.backups.operations.destroy.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
backup
.destroy()
.then(() =>
this.controllerFor("adminBackupsIndex")
.get("model")
.removeObject(backup)
);
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.operations.destroy.confirm"),
didConfirm: () => {
backup
.destroy()
.then(() =>
this.controllerFor("adminBackupsIndex")
.get("model")
.removeObject(backup)
);
},
});
},
startRestore(backup) {
bootbox.confirm(
I18n.t("admin.backups.operations.restore.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
this.transitionTo("admin.backups.logs");
backup.restore();
}
}
);
this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.operations.restore.confirm"),
didConfirm: () => {
this.transitionTo("admin.backups.logs");
backup.restore();
},
});
},
cancelOperation() {
bootbox.confirm(
I18n.t("admin.backups.operations.cancel.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
Backup.cancel().then(() => {
this.controllerFor("adminBackups").set(
"model.isOperationRunning",
false
);
});
}
}
);
this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.operations.cancel.confirm"),
didConfirm: () => {
Backup.cancel().then(() => {
this.controllerFor("adminBackups").set(
"model.isOperationRunning",
false
);
});
},
});
},
rollback() {
bootbox.confirm(
I18n.t("admin.backups.operations.rollback.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
Backup.rollback().then((result) => {
if (!result.success) {
bootbox.alert(result.message);
} else {
// redirect to homepage (session might be lost)
window.location = getURL("/");
}
});
}
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.operations.rollback.confirm"),
didConfirm: () => {
Backup.rollback().then((result) => {
if (!result.success) {
this.dialog.alert(result.message);
} else {
// redirect to homepage (session might be lost)
window.location = getURL("/");
}
});
},
});
},
uploadSuccess(filename) {
bootbox.alert(I18n.t("admin.backups.upload.success", { filename }));
this.dialog.alert(I18n.t("admin.backups.upload.success", { filename }));
},
uploadError(filename, message) {
bootbox.alert(
this.dialog.alert(
I18n.t("admin.backups.upload.error", { filename, message })
);
},
@ -173,7 +158,7 @@ export default DiscourseRoute.extend({
);
})
.catch((error) => {
bootbox.alert(
this.dialog.alert(
I18n.t("admin.backups.backup_storage_error", {
error_message: extractError(error),
})

View File

@ -2,11 +2,13 @@ import Badge from "discourse/models/badge";
import I18n from "I18n";
import Route from "@ember/routing/route";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { action, get } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
export default class AdminBadgesShowRoute extends Route {
@service dialog;
serialize(m) {
return { badge_id: get(m, "id") || "new" };
}
@ -58,7 +60,7 @@ export default class AdminBadgesShowRoute extends Route {
badge.set("preview_loading", false);
// eslint-disable-next-line no-console
console.error(error);
bootbox.alert("Network error");
this.dialog.alert("Network error");
});
}
}

View File

@ -1,7 +1,7 @@
import AdminUser from "admin/models/admin-user";
import I18n from "I18n";
import { Promise } from "rsvp";
import Service from "@ember/service";
import Service, { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { getOwner } from "discourse-common/lib/get-owner";
@ -12,6 +12,8 @@ import showModal from "discourse/lib/show-modal";
// and the admin application. Use this if you need front end code to access admin
// modules. Inject it optionally, and if it exists go to town!
export default Service.extend({
dialog: service(),
showActionLogs(target, filters) {
const controller = getOwner(target).lookup(
"controller:adminLogs.staffActionLogs"
@ -120,7 +122,7 @@ export default Service.extend({
}
})
.catch(() => {
bootbox.alert(I18n.t("admin.user.delete_failed"));
this.dialog.alert(I18n.t("admin.user.delete_failed"));
reject();
});
},

View File

@ -97,21 +97,21 @@
<div class="control-group">
<label>
<Input @type="checkbox" @checked={{this.buffered.auto_revoke}} disabled={{this.readOnly}} />
<Input name="auto_revoke" @type="checkbox" @checked={{this.buffered.auto_revoke}} disabled={{this.readOnly}} />
{{i18n "admin.badges.auto_revoke"}}
</label>
</div>
<div class="control-group">
<label>
<Input @type="checkbox" @checked={{this.buffered.target_posts}} disabled={{this.readOnly}} />
<Input name="target_posts" @type="checkbox" @checked={{this.buffered.target_posts}} disabled={{this.readOnly}} />
{{i18n "admin.badges.target_posts"}}
</label>
</div>
<div class="control-group">
<label for="trigger">{{i18n "admin.badges.trigger"}}</label>
<ComboBox @name="trigger" @value={{this.buffered.trigger}} @content={{this.badgeTriggers}} @onChange={{action (mut this.buffered.trigger)}} @options={{hash
<ComboBox name="trigger" @value={{this.buffered.trigger}} @content={{this.badgeTriggers}} @onChange={{action (mut this.buffered.trigger)}} @options={{hash
disabled=this.readOnly
}} />
</div>

View File

@ -0,0 +1,3 @@
<div class="composer-fullscreen-prompt" {{on "animationend" @removeFullScreenExitPrompt}}>
{{html-safe (i18n "composer.exit_fullscreen_prompt")}}
</div>

View File

@ -63,9 +63,7 @@
<td class="col actions">
{{#if item.editing}}
<DButton @class="btn-default" @action={{action "save"}} @actionParam={{item}} @label="admin.logs.save" />
<a href {{action "cancel" item}} class="cancel-action">
{{i18n "cancel"}}
</a>
<DButton @class="btn-flat" @action={{action "cancel" item}} @translatedLabel={{i18n "cancel"}} />
{{else}}
<DButton @class="btn-default btn-danger" @action={{action "destroy"}} @actionParam={{item}} @icon="far-trash-alt" />
<DButton @class="btn-default" @action={{action "edit"}} @actionParam={{item}} @icon="pencil-alt" />

View File

@ -1,39 +1,60 @@
<div class="search-area">
<p>{{i18n "admin.site_text.description"}}</p>
<TextField @value={{this.q}} @placeholderKey="admin.site_text.search" @class="no-blur site-text-search" @autofocus="true" @key-up={{action "search"}} />
<TextField
@value={{this.q}}
@placeholderKey="admin.site_text.search"
@class="no-blur site-text-search"
@autofocus="true"
@key-up={{action "search"}}
/>
<div class="reseed">
<DButton @action={{route-action "showReseedModal"}} @class="btn-default" @label="admin.reseed.action.label" @title="admin.reseed.action.title" @icon="sync" />
<DButton
@action={{route-action "showReseedModal"}}
@class="btn-default"
@label="admin.reseed.action.label"
@title="admin.reseed.action.title"
@icon="sync"
/>
</div>
<p class="filter-options">
<div class="locale">
<label>{{i18n "admin.site_text.locale"}}</label>
<ComboBox @valueProperty="value" @content={{this.availableLocales}} @value={{this.locale}} @onChange={{action "updateLocale"}} @class="locale-search" @options={{hash filterable=true}} />
<ComboBox
@valueProperty="value"
@content={{this.availableLocales}}
@value={{this.locale}}
@onChange={{action "updateLocale"}}
@class="locale-search"
@options={{hash filterable=true}}
/>
</div>
<label>
<Input @type="checkbox" @checked={{this.overridden}} {{on "click" (action "toggleOverridden")}} />
<Input
@type="checkbox"
@checked={{this.overridden}}
{{on "click" (action "toggleOverridden")}}
/>
{{i18n "admin.site_text.show_overriden"}}
</label>
</p>
</div>
<ConditionalLoadingSpinner @condition={{this.searching}}>
{{#if this.showFallbackLocaleWarning}}
<div class="alert alert-info">
{{d-icon "exclamation-circle"}}
{{i18n "admin.site_text.fallback_locale_warning" fallback=this.fallbackLocaleFullName}}
</div>
{{/if}}
{{#if this.siteTexts.extras.recommended}}
<p><b>{{i18n "admin.site_text.recommended"}}</b></p>
{{/if}}
{{#each this.siteTexts as |siteText|}}
<SiteTextSummary @siteText={{siteText}} @editAction={{action "edit"}} @term={{this.q}} @searchRegex={{this.siteTexts.extras.regex}} />
<SiteTextSummary
@siteText={{siteText}}
@editAction={{action "edit"}}
@term={{this.q}}
@searchRegex={{this.siteTexts.extras.regex}}
/>
{{/each}}
{{#if this.siteTexts.extras.has_more}}

View File

@ -17,9 +17,9 @@
"dependencies": {
"ember-auto-import": "^2.4.2",
"ember-cli-babel": "^7.26.10",
"ember-cli-htmlbars": "^6.1.0",
"webpack": "^5.73.0",
"xss": "^1.0.13"
"ember-cli-htmlbars": "^6.1.1",
"webpack": "^5.74.0",
"xss": "^1.0.14"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",

View File

@ -27,7 +27,7 @@ const REPLACEMENTS = {
"notification.group_mentioned": "users",
"notification.quoted": "quote-right",
"notification.replied": "reply",
"notification.posted": "reply",
"notification.posted": "discourse-bell-exclamation",
"notification.edited": "pencil-alt",
"notification.bookmark_reminder": "discourse-bookmark-clock",
"notification.liked": "heart",

View File

@ -73,9 +73,10 @@ if (Handlebars.Compiler) {
RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars";
RawHandlebars.precompile = function (value, asObject) {
RawHandlebars.precompile = function (value, asObject, { plugins = [] } = {}) {
let ast = Handlebars.parse(value);
replaceGet(ast);
plugins.forEach((plugin) => plugin(ast));
let options = {
knownHelpers: {
@ -96,9 +97,10 @@ if (Handlebars.Compiler) {
);
};
RawHandlebars.compile = function (string) {
RawHandlebars.compile = function (string, { plugins = [] } = {}) {
let ast = Handlebars.parse(string);
replaceGet(ast);
plugins.forEach((plugin) => plugin(ast));
// this forces us to rewrite helpers
let options = { data: true, stringParams: true };

View File

@ -38,55 +38,68 @@ const DEPRECATED_MODULES = new Map(
since: "2.4.0",
dropFrom: "2.9.0.beta1",
},
// Deprecations below are silenced because they're in widespread use, and upgrading
// themes/plugins right now would break their compatibility with the stable branch.
// These should be unsilenced for the release of 2.9.0 stable.
"store:main": {
newName: "service:store",
since: "2.8.0.beta8",
dropFrom: "2.9.0.beta1",
silent: true,
},
"search-service:main": {
newName: "service:search",
since: "2.8.0.beta8",
dropFrom: "2.9.0.beta1",
silent: true,
},
"key-value-store:main": {
newName: "service:key-value-store",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"pm-topic-tracking-state:main": {
newName: "service:pm-topic-tracking-state",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"message-bus:main": {
newName: "service:message-bus",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"site-settings:main": {
newName: "service:site-settings",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"current-user:main": {
newName: "service:current-user",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"session:main": {
newName: "service:session",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"site:main": {
newName: "service:site",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
"topic-tracking-state:main": {
newName: "service:topic-tracking-state",
since: "2.9.0.beta7",
dropFrom: "3.0.0",
silent: true,
},
})
);
@ -138,13 +151,15 @@ export function buildResolver(baseName) {
_normalize(fullName) {
const deprecationInfo = DEPRECATED_MODULES.get(fullName);
if (deprecationInfo) {
deprecated(
`"${fullName}" is deprecated, use "${deprecationInfo.newName}" instead`,
{
since: deprecationInfo.since,
dropFrom: deprecationInfo.dropFrom,
}
);
if (!deprecationInfo.silent) {
deprecated(
`"${fullName}" is deprecated, use "${deprecationInfo.newName}" instead`,
{
since: deprecationInfo.since,
dropFrom: deprecationInfo.dropFrom,
}
);
}
fullName = deprecationInfo.newName;
}

View File

@ -1,17 +1,18 @@
import EmberObject, { computed, get } from "@ember/object";
import CoreObject from "@ember/object/core";
import { computed, get } from "@ember/object";
import extractValue from "./extract-value";
export default function handleDescriptor(target, key, desc, params = []) {
const val = extractValue(desc);
if (typeof val === "function" && target instanceof EmberObject) {
if (typeof val === "function" && target instanceof CoreObject) {
// We're in a native class, so convert the method to a getter first
desc.writable = false;
desc.initializer = undefined;
desc.value = undefined;
desc.get = callUserSuppliedGet(params, val);
return computed(target, key, desc);
return computed(...params)(target, key, desc);
} else {
return {
enumerable: desc.enumerable,

View File

@ -23,11 +23,11 @@
"@uppy/xhr-upload": "^2.1.2",
"ember-auto-import": "^2.4.2",
"ember-cli-babel": "^7.26.10",
"ember-cli-htmlbars": "^6.1.0",
"ember-cli-htmlbars": "^6.1.1",
"ember-resolver": "^8.0.3",
"handlebars": "^4.7.0",
"truth-helpers": "^1.0.0",
"webpack": "^5.73.0"
"webpack": "^5.74.0"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",

View File

@ -17,9 +17,9 @@
"dependencies": {
"ember-auto-import": "^2.4.2",
"ember-cli-babel": "^7.26.10",
"ember-cli-htmlbars": "^6.1.0",
"ember-cli-htmlbars": "^6.1.1",
"handlebars": "^4.7.6",
"webpack": "^5.73.0"
"webpack": "^5.74.0"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",

View File

@ -134,7 +134,19 @@ TemplateCompiler.prototype.initializeFeatures =
function initializeFeatures() {};
TemplateCompiler.prototype.processString = function (string, relativePath) {
let filename = relativePath.replace(/^templates\//, "").replace(/\.hbr$/, "");
let filename;
const pluginName = relativePath.match(/^discourse\/plugins\/([^\/]+)\//)?.[1];
if (pluginName) {
filename = relativePath
.replace(`discourse/plugins/${pluginName}/`, "")
.replace(/^(discourse\/)?templates\//, "javascripts/");
} else {
filename = relativePath.replace(/^templates\//, "");
}
filename = filename.replace(/\.hbr$/, "");
return (
'import { template as compiler } from "discourse-common/lib/raw-handlebars";\n' +

View File

@ -0,0 +1,110 @@
/* global Babel:true */
// This is executed in mini_racer to provide the JS logic for lib/discourse_js_processor.rb
const makeEmberTemplateCompilerPlugin =
require("babel-plugin-ember-template-compilation").default;
const precompile = require("ember-template-compiler").precompile;
const Handlebars = require("handlebars").default;
function manipulateAstNodeForTheme(node, themeId) {
// Magically add theme id as the first param for each of these helpers)
if (
node.path.parts &&
["theme-i18n", "theme-prefix", "theme-setting"].includes(node.path.parts[0])
) {
if (node.params.length === 1) {
node.params.unshift({
type: "NumberLiteral",
value: themeId,
original: themeId,
loc: { start: {}, end: {} },
});
}
}
}
function buildEmberTemplateManipulatorPlugin(themeId) {
return function () {
return {
name: "theme-template-manipulator",
visitor: {
SubExpression: (node) => manipulateAstNodeForTheme(node, themeId),
MustacheStatement: (node) => manipulateAstNodeForTheme(node, themeId),
},
};
};
}
function buildTemplateCompilerBabelPlugins({ themeId }) {
let compileFunction = precompile;
if (themeId) {
compileFunction = (src, opts) => {
return precompile(src, {
...opts,
plugins: {
ast: [buildEmberTemplateManipulatorPlugin(themeId)],
},
});
};
}
return [
require("widget-hbs-compiler").WidgetHbsCompiler,
[
makeEmberTemplateCompilerPlugin(() => compileFunction),
{ enableLegacyModules: ["ember-cli-htmlbars"] },
],
];
}
function buildThemeRawHbsTemplateManipulatorPlugin(themeId) {
return function (ast) {
["SubExpression", "MustacheStatement"].forEach((pass) => {
let visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor[pass] = (node) => manipulateAstNodeForTheme(node, themeId);
visitor.accept(ast);
});
};
}
exports.compileRawTemplate = function (source, themeId) {
try {
const RawHandlebars = require("raw-handlebars").default;
const plugins = [];
if (themeId) {
plugins.push(buildThemeRawHbsTemplateManipulatorPlugin(themeId));
}
return RawHandlebars.precompile(source, false, { plugins }).toString();
} catch (error) {
// Workaround for https://github.com/rubyjs/mini_racer/issues/262
error.message = JSON.stringify(error.message);
throw error;
}
};
exports.transpile = function (
source,
{ moduleId, filename, skipModule, themeId, commonPlugins } = {}
) {
const plugins = [];
plugins.push(...buildTemplateCompilerBabelPlugins({ themeId }));
if (moduleId && !skipModule) {
plugins.push(["transform-modules-amd", { noInterop: true }]);
}
plugins.push(...commonPlugins);
try {
return Babel.transform(source, {
moduleId,
filename,
ast: false,
plugins,
}).code;
} catch (error) {
// Workaround for https://github.com/rubyjs/mini_racer/issues/262
error.message = JSON.stringify(error.message);
throw error;
}
};

View File

@ -0,0 +1,213 @@
"use strict";
const path = require("path");
const WatchedDir = require("broccoli-source").WatchedDir;
const Funnel = require("broccoli-funnel");
const mergeTrees = require("broccoli-merge-trees");
const fs = require("fs");
const concat = require("broccoli-concat");
const RawHandlebarsCompiler = require("discourse-hbr/raw-handlebars-compiler");
function fixLegacyExtensions(tree) {
return new Funnel(tree, {
getDestinationPath: function (relativePath) {
if (relativePath.endsWith(".es6")) {
return relativePath.slice(0, -4);
} else if (relativePath.endsWith(".raw.hbs")) {
return relativePath.replace(".raw.hbs", ".hbr");
}
return relativePath;
},
});
}
const COLOCATED_CONNECTOR_REGEX =
/^(?<prefix>.*)\/connectors\/(?<outlet>[^\/]+)\/(?<name>[^\/\.]+)\.(?<extension>.+)$/;
// Having connector templates and js in the same directory causes a clash
// when outputting es6 modules. This shim separates colocated connectors
// into separate js / template locations.
function unColocateConnectors(tree) {
return new Funnel(tree, {
getDestinationPath: function (relativePath) {
const match = relativePath.match(COLOCATED_CONNECTOR_REGEX);
if (
match &&
match.groups.extension === "hbs" &&
!match.groups.prefix.endsWith("/templates")
) {
const { prefix, outlet, name } = match.groups;
return `${prefix}/templates/connectors/${outlet}/${name}.hbs`;
}
if (
match &&
match.groups.extension === "js" &&
match.groups.prefix.endsWith("/templates")
) {
// Some plugins are colocating connector JS under `/templates`
const { prefix, outlet, name } = match.groups;
const newPrefix = prefix.slice(0, -"/templates".length);
return `${newPrefix}/connectors/${outlet}/${name}.js`;
}
return relativePath;
},
});
}
function namespaceModules(tree, pluginDirectoryName) {
return new Funnel(tree, {
getDestinationPath: function (relativePath) {
return `discourse/plugins/${pluginDirectoryName}/${relativePath}`;
},
});
}
function parsePluginName(pluginRbPath) {
const pluginRb = fs.readFileSync(pluginRbPath, "utf8");
// Match parsing logic in `lib/plugin/metadata.rb`
for (const line of pluginRb.split("\n")) {
if (line.startsWith("#")) {
const [attribute, value] = line.slice(1).split(":", 2);
if (attribute.trim() === "name") {
return value.trim();
}
}
}
throw new Error(
`Unable to parse plugin name from metadata in ${pluginRbPath}`
);
}
module.exports = {
name: require("./package").name,
pluginInfos() {
const root = path.resolve("../../../../plugins");
const pluginDirectories = fs
.readdirSync(root, { withFileTypes: true })
.filter(
(dirent) =>
(dirent.isDirectory() || dirent.isSymbolicLink()) &&
!dirent.name.startsWith(".") &&
fs.existsSync(path.resolve(root, dirent.name, "plugin.rb"))
);
return pluginDirectories.map((directory) => {
const directoryName = directory.name;
const pluginName = parsePluginName(
path.resolve(root, directoryName, "plugin.rb")
);
const jsDirectory = path.resolve(
root,
directoryName,
"assets/javascripts"
);
const adminJsDirectory = path.resolve(
root,
directoryName,
"admin/assets/javascripts"
);
const testDirectory = path.resolve(
root,
directoryName,
"test/javascripts"
);
const hasJs = fs.existsSync(jsDirectory);
const hasAdminJs = fs.existsSync(adminJsDirectory);
const hasTests = fs.existsSync(testDirectory);
return {
pluginName,
directoryName,
jsDirectory,
adminJsDirectory,
testDirectory,
hasJs,
hasAdminJs,
hasTests,
};
});
},
generatePluginsTree() {
const appTree = this._generatePluginAppTree();
const testTree = this._generatePluginTestTree();
const adminTree = this._generatePluginAdminTree();
return mergeTrees([appTree, testTree, adminTree]);
},
_generatePluginAppTree() {
const trees = this.pluginInfos()
.filter((p) => p.hasJs)
.map(({ pluginName, directoryName, jsDirectory }) =>
this._buildAppTree({
directory: jsDirectory,
pluginName,
outputFile: `assets/plugins/${directoryName}.js`,
})
);
return mergeTrees(trees);
},
_generatePluginAdminTree() {
const trees = this.pluginInfos()
.filter((p) => p.hasAdminJs)
.map(({ pluginName, directoryName, adminJsDirectory }) =>
this._buildAppTree({
directory: adminJsDirectory,
pluginName,
outputFile: `assets/plugins/${directoryName}_admin.js`,
})
);
return mergeTrees(trees);
},
_buildAppTree({ directory, pluginName, outputFile }) {
let tree = new WatchedDir(directory);
tree = fixLegacyExtensions(tree);
tree = unColocateConnectors(tree);
tree = namespaceModules(tree, pluginName);
tree = RawHandlebarsCompiler(tree);
tree = this.compileTemplates(tree);
tree = this.processedAddonJsFiles(tree);
return concat(mergeTrees([tree]), {
inputFiles: ["**/*.js"],
outputFile,
allowNone: true,
});
},
_generatePluginTestTree() {
const trees = this.pluginInfos()
.filter((p) => p.hasTests)
.map(({ pluginName, directoryName, testDirectory }) => {
let tree = new WatchedDir(testDirectory);
tree = fixLegacyExtensions(tree);
tree = namespaceModules(tree, pluginName);
tree = this.processedAddonJsFiles(tree);
return concat(mergeTrees([tree]), {
inputFiles: ["**/*.js"],
outputFile: `assets/plugins/test/${directoryName}_tests.js`,
allowNone: true,
});
});
return mergeTrees(trees);
},
shouldCompileTemplates() {
// The base Addon implementation checks for template
// files in the addon directories. We need to override that
// check so that the template compiler always runs.
return true;
},
treeFor() {
// This addon doesn't contribute any 'real' trees to the app
return;
},
};

View File

@ -0,0 +1,25 @@
{
"name": "discourse-plugins",
"version": "1.0.0",
"description": "An addon providing a broccoli tree for each Discourse plugin",
"author": "Discourse",
"license": "GPL-2.0-only",
"keywords": [
"ember-addon"
],
"repository": "",
"dependencies": {
"ember-auto-import": "^2.4.2",
"ember-cli-babel": "^7.26.10",
"ember-cli-htmlbars": "^6.1.1",
"discourse-widget-hbs": "1.0"
},
"engines": {
"node": "16.* || >= 18",
"npm": "please-use-yarn",
"yarn": ">= 1.21.1"
},
"ember": {
"edition": "default"
}
}

View File

@ -17,9 +17,9 @@
"dependencies": {
"ember-auto-import": "^2.4.2",
"ember-cli-babel": "^7.26.10",
"ember-cli-htmlbars": "^6.1.0",
"ember-cli-htmlbars": "^6.1.1",
"handlebars": "^4.7.6",
"webpack": "^5.73.0"
"webpack": "^5.74.0"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",

View File

@ -47,20 +47,22 @@ export default EmberObject.extend({
appendQueryParams(path, findArgs, extension) {
if (findArgs) {
if (typeof findArgs === "object") {
const queryString = Object.keys(findArgs)
.reject((k) => !findArgs[k])
.map((k) => k + "=" + encodeURIComponent(findArgs[k]));
const urlSearchParams = new URLSearchParams();
if (queryString.length) {
return `${path}${extension ? extension : ""}?${queryString.join(
"&"
)}`;
for (const [key, value] of Object.entries(findArgs)) {
if (value) {
urlSearchParams.set(key, value);
}
}
const queryString = urlSearchParams.toString();
if (queryString) {
return `${path}${extension || ""}?${queryString}`;
}
} else {
// It's serializable as a string if not an object
return `${path}/${encodeURIComponent(findArgs)}${
extension ? extension : ""
}`;
return `${path}/${encodeURIComponent(findArgs)}${extension || ""}`;
}
}
return path;

View File

@ -8,18 +8,18 @@ export function finderFor(filter, params) {
let url = getURL("/") + filter + ".json";
if (params) {
const keys = Object.keys(params),
encoded = [];
const urlSearchParams = new URLSearchParams();
keys.forEach(function (p) {
const value = encodeURI(params[p]);
for (const [key, value] of Object.entries(params)) {
if (typeof value !== "undefined") {
encoded.push(p + "=" + value);
urlSearchParams.set(key, value);
}
});
}
if (encoded.length > 0) {
url += "?" + encoded.join("&");
const queryString = urlSearchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
return ajax(url);
@ -32,7 +32,7 @@ export default RestAdapter.extend({
const params = findArgs.params;
return PreloadStore.getAndRemove(
"topic_list_" + filter,
"topic_list",
finderFor(filter, params)
).then(function (result) {
result.filter = filter;

View File

@ -2,13 +2,12 @@ import Component from "@ember/component";
import { action } from "@ember/object";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { inject as service } from "@ember/service";
export default Component.extend({
dialog: service(),
tagName: "",
selectableUserBadges: null,
_selectedUserBadgeId: null,
_isSaved: false,
_isSaving: false,
@ -42,7 +41,7 @@ export default Component.extend({
this.currentUser.set("title", selectedUserBadge?.badge?.name || "");
},
() => {
bootbox.alert(I18n.t("generic_error"));
this.dialog.alert(I18n.t("generic_error"));
}
)
.finally(() => this.set("_isSaving", false));

View File

@ -50,11 +50,7 @@ export default Component.extend({
`.indicator-topic-${data.topic_id}`
).classList;
if (data.show_indicator) {
nodeClassList.remove("read");
} else {
nodeClassList.add("read");
}
nodeClassList.toggle("read", !data.show_indicator);
});
}
});

View File

@ -18,14 +18,16 @@ import { and, notEmpty } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseLater from "discourse-common/lib/later";
import { inject as service } from "@ember/service";
const BOOKMARK_BINDINGS = {
enter: { handler: "saveAndClose" },
"d d": { handler: "delete" },
};
export default Component.extend({
dialog: service(),
tagName: "",
errorMessage: null,
selectedReminderType: null,
_closeWithoutSaving: null,
@ -227,7 +229,7 @@ export default Component.extend({
_handleSaveError(e) {
this._savingBookmarkManually = false;
if (typeof e === "string") {
bootbox.alert(e);
this.dialog.alert(e);
} else {
popupAjaxError(e);
}

View File

@ -1,9 +1,11 @@
import GlimmerComponent from "@glimmer/component";
import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
export default class BootstrapModeNotice extends GlimmerComponent {
export default class BootstrapModeNotice extends Component {
@service siteSettings;
@service site;
@ -19,4 +21,9 @@ export default class BootstrapModeNotice extends GlimmerComponent {
return htmlSafe(I18n.t(msg, { count: bootstrapModeMinUsers }));
}
@action
inviteUsers() {
showModal("create-invite");
}
}

View File

@ -89,11 +89,6 @@ export default Component.extend(KeyEnterEscape, {
passive: false,
});
});
if (this._visualViewportResizing()) {
this.viewportResize();
window.visualViewport.addEventListener("resize", this.viewportResize);
}
},
@bind
@ -107,6 +102,11 @@ export default Component.extend(KeyEnterEscape, {
const minHeight = parseInt(getComputedStyle(this.element).minHeight, 10);
size = Math.max(minHeight, size);
this.set("composer.composerHeight", `${size}px`);
this.keyValueStore.set({
key: "composerHeight",
value: this.get("composer.composerHeight"),
});
document.documentElement.style.setProperty(
"--composer-height",
size ? `${size}px` : ""
@ -169,42 +169,9 @@ export default Component.extend(KeyEnterEscape, {
throttle(this, this.performDragHandler, event, THROTTLE_RATE);
},
@bind
viewportResize() {
const composerVH = window.visualViewport.height * 0.01,
doc = document.documentElement;
doc.style.setProperty("--composer-vh", `${composerVH}px`);
const viewportWindowDiff =
this.windowInnerHeight - window.visualViewport.height;
viewportWindowDiff > 0
? doc.classList.add("keyboard-visible")
: doc.classList.remove("keyboard-visible");
// adds bottom padding when using a hardware keyboard and the accessory bar is visible
// accessory bar height is 55px, using 75 allows a small buffer
doc.style.setProperty(
"--composer-ipad-padding",
`${viewportWindowDiff < 75 ? viewportWindowDiff : 0}px`
);
},
_visualViewportResizing() {
return (
(this.capabilities.isIpadOS || this.site.mobileView) &&
window.visualViewport !== undefined
);
},
didInsertElement() {
this._super(...arguments);
if (this._visualViewportResizing()) {
this.set("windowInnerHeight", window.innerHeight);
}
this.setupComposerResizeEvents();
const triggerOpen = () => {
@ -224,10 +191,6 @@ export default Component.extend(KeyEnterEscape, {
willDestroyElement() {
this._super(...arguments);
if (this._visualViewportResizing()) {
window.visualViewport.removeEventListener("resize", this.viewportResize);
}
START_DRAG_EVENTS.forEach((startDragEvent) => {
this.element
.querySelector(".grippie")

View File

@ -816,7 +816,6 @@ export default Component.extend(ComposerUploadUppy, {
extraButtons(toolbar) {
toolbar.addButton({
tabindex: "0",
id: "quote",
group: "fontStyles",
icon: "far-comment",

View File

@ -0,0 +1,3 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -5,8 +5,10 @@ import LinkLookup from "discourse/lib/link-lookup";
import { not } from "@ember/object/computed";
import { scheduleOnce } from "@ember/runloop";
import showModal from "discourse/lib/show-modal";
import { ajax } from "discourse/lib/ajax";
let _messagesCache = {};
let _recipient_names = [];
export default Component.extend({
classNameBindings: [":composer-popup-container", "hidden"],
@ -18,6 +20,7 @@ export default Component.extend({
_similarTopicsMessage: null,
_yourselfConfirm: null,
similarTopics: null,
usersNotSeen: null,
hidden: not("composer.viewOpenOrFullscreen"),
@ -119,6 +122,53 @@ export default Component.extend({
const composer = this.composer;
if (composer.get("privateMessage")) {
const recipients = composer.targetRecipientsArray;
const recipient_names = recipients
.filter((r) => r.type === "user")
.map(({ name }) => name);
if (
recipient_names.length > 0 &&
recipient_names.length !== _recipient_names.length &&
!recipient_names.every((v, i) => v === _recipient_names[i])
) {
_recipient_names = recipient_names;
ajax(`/composer_messages/user_not_seen_in_a_while`, {
type: "GET",
data: {
usernames: recipient_names,
},
}).then((response) => {
if (
response.user_count > 0 &&
this.get("usersNotSeen") !== response.usernames.join("-")
) {
this.set("usersNotSeen", response.usernames.join("-"));
this.messagesByTemplate["education"] = undefined;
let usernames = [];
response.usernames.forEach((username, index) => {
usernames[
index
] = `<a class='mention' href='/u/${username}'>@${username}</a>`;
});
let body_key = "composer.user_not_seen_in_a_while.single";
if (response.user_count > 1) {
body_key = "composer.user_not_seen_in_a_while.multiple";
}
const message = composer.store.createRecord("composer-message", {
id: "user-not-seen",
templateName: "education",
body: I18n.t(body_key, {
usernames: usernames.join(", "),
time_ago: response.time_ago,
}),
});
this.send("popup", message);
}
});
}
if (
recipients.length > 0 &&
@ -128,7 +178,7 @@ export default Component.extend({
this._yourselfConfirm ||
composer.store.createRecord("composer-message", {
id: "yourself_confirm",
templateName: "custom-body",
templateName: "education",
title: I18n.t("composer.yourself_confirm.title"),
body: I18n.t("composer.yourself_confirm.body"),
});

View File

@ -4,11 +4,13 @@ import I18n from "I18n";
import LivePostCounts from "discourse/models/live-post-counts";
import { alias } from "@ember/object/computed";
import { htmlSafe } from "@ember/template";
import { inject as service } from "@ember/service";
export default Component.extend({
classNameBindings: ["hidden:hidden", ":create-topics-notice"],
enabled: false,
router: service(),
publicTopicCount: null,
publicPostCount: null,
@ -37,14 +39,16 @@ export default Component.extend({
}
},
@discourseComputed()
shouldSee() {
const user = this.currentUser;
@discourseComputed(
"siteSettings.show_create_topics_notice",
"router.currentRouteName"
)
shouldSee(showCreateTopicsNotice, currentRouteName) {
return (
user &&
user.get("admin") &&
this.siteSettings.show_create_topics_notice &&
!this.site.get("wizard_required")
this.currentUser?.get("admin") &&
showCreateTopicsNotice &&
!this.site.get("wizard_required") &&
!currentRouteName.startsWith("wizard")
);
},

View File

@ -1,7 +1,6 @@
import Component from "@ember/component";
import I18n from "I18n";
import { bind } from "discourse-common/utils/decorators";
import bootbox from "bootbox";
import logout from "discourse/lib/logout";
import { inject as service } from "@ember/service";
import { setLogoffCallback } from "discourse/lib/ajax";
@ -14,6 +13,7 @@ export function addPluginDocumentTitleCounter(counterFunction) {
export default Component.extend({
tagName: "",
documentTitle: service(),
dialog: service(),
_showingLogout: false,
didInsertElement() {
@ -44,16 +44,23 @@ export default Component.extend({
);
},
_updateNotifications() {
_updateNotifications(opts) {
if (!this.currentUser) {
return;
}
const count =
pluginCounterFunctions.reduce((sum, fn) => sum + fn(), 0) +
this.currentUser.unread_notifications +
this.currentUser.unread_high_priority_notifications;
this.documentTitle.updateNotificationCount(count);
let count = pluginCounterFunctions.reduce((sum, fn) => sum + fn(), 0);
if (this.currentUser.redesigned_user_menu_enabled) {
count += this.currentUser.all_unread_notifications_count;
if (this.currentUser.unseen_reviewable_count) {
count += this.currentUser.unseen_reviewable_count;
}
} else {
count +=
this.currentUser.unread_notifications +
this.currentUser.unread_high_priority_notifications;
}
this.documentTitle.updateNotificationCount(count, { forced: opts?.forced });
},
@bind
@ -74,13 +81,12 @@ export default Component.extend({
this._showingLogout = true;
this.messageBus.stop();
bootbox.dialog(
I18n.t("logout"),
{ label: I18n.t("refresh"), callback: logout },
{
onEscape: () => logout(),
backdrop: "static",
}
);
this.dialog.alert({
message: I18n.t("logout"),
confirmButtonLabel: "refresh",
didConfirm: () => logout(),
didCancel: () => logout(),
});
},
});

View File

@ -50,7 +50,7 @@ let _createCallbacks = [];
class Toolbar {
constructor(opts) {
const { site, siteSettings } = opts;
const { siteSettings, capabilities } = opts;
this.shortcuts = {};
this.context = null;
@ -106,16 +106,16 @@ class Toolbar {
}),
});
this.addButton({
id: "code",
group: "insertions",
shortcut: "E",
preventFocus: true,
trimLeading: true,
action: (...args) => this.context.send("formatCode", args),
});
if (!capabilities.touch) {
this.addButton({
id: "code",
group: "insertions",
shortcut: "E",
preventFocus: true,
trimLeading: true,
action: (...args) => this.context.send("formatCode", args),
});
if (!site.mobileView) {
this.addButton({
id: "bullet",
group: "extras",
@ -354,7 +354,7 @@ export default Component.extend(TextareaTextManipulation, {
@discourseComputed()
toolbar() {
const toolbar = new Toolbar(
this.getProperties("site", "siteSettings", "showLink")
this.getProperties("site", "siteSettings", "showLink", "capabilities")
);
toolbar.context = this;
@ -363,6 +363,12 @@ export default Component.extend(TextareaTextManipulation, {
if (this.extraButtons) {
this.extraButtons(toolbar);
}
const firstButton = toolbar.groups.mapBy("buttons").flat().firstObject;
if (firstButton) {
firstButton.tabindex = 0;
}
return toolbar;
},
@ -702,6 +708,7 @@ export default Component.extend(TextareaTextManipulation, {
this.applySurround(selected, head, tail, exampleKey, opts),
applyList: (head, exampleKey, opts) =>
this._applyList(selected, head, exampleKey, opts),
formatCode: (...args) => this.send("formatCode", args),
addText: (text) => this.addText(selected, text),
getText: () => this.value,
toggleDirection: () => this._toggleDirection(),

View File

@ -1,7 +1,6 @@
import Component from "@ember/component";
import FilterModeMixin from "discourse/mixins/filter-mode";
import NavItem from "discourse/models/nav-item";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { NotificationLevels } from "discourse/lib/notification-levels";
import { getOwner } from "discourse-common/lib/get-owner";
@ -9,7 +8,7 @@ import { inject as service } from "@ember/service";
export default Component.extend(FilterModeMixin, {
router: service(),
dialog: service(),
tagName: "",
// Should be a `readOnly` instead but some themes/plugins still pass
@ -158,7 +157,7 @@ export default Component.extend(FilterModeMixin, {
clickCreateTopicButton() {
if (this.categoryReadOnlyBanner && !this.hasDraft) {
bootbox.alert(this.categoryReadOnlyBanner);
this.dialog.alert(this.categoryReadOnlyBanner);
} else {
this.createTopic();
}

View File

@ -26,6 +26,7 @@ export default class DiscourseTooltip extends Component {
const parent = viewBounds.parentElement;
this._tippyInstance = tippy(parent, {
content: element,
trigger: this.capabilities.touch ? "click" : "mouseenter",
theme: "d-tooltip",
arrow: false,
placement: "bottom-start",

View File

@ -41,6 +41,10 @@ export default Component.extend({
usePopper: true,
placement: "auto", // one of popper.js' placements, see https://popper.js.org/docs/v2/constructors/#options
initialFilter: "",
elements: {
searchInput: ".emoji-picker-search-container input",
picker: ".emoji-picker-emoji-area",
},
init() {
this._super(...arguments);
@ -245,8 +249,116 @@ export default Component.extend({
@action
keydown(event) {
if (event.code === "Escape") {
const arrowKeys = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"];
const emojis = document.querySelectorAll(".emoji-picker-emoji-area .emoji");
let currentEmoji;
this.set(
"hoveredEmoji",
this._codeWithDiversity(event.target.title, this.selectedDiversity)
);
if (
event.key === "ArrowDown" &&
this._focusedOn(this.elements.searchInput)
) {
emojis[0].focus();
event.preventDefault();
return false;
}
if (event.key === "Escape") {
this.onClose(event);
const path = event.path || (event.composedPath && event.composedPath());
const fromChatComposer = path.find((e) =>
e?.classList?.contains("chat-composer-container")
);
const fromTopicComposer = path.find((e) =>
e?.classList?.contains("d-editor")
);
if (fromTopicComposer) {
document.querySelector(".d-editor-input")?.focus();
} else if (fromChatComposer) {
document.querySelector(".chat-composer-input")?.focus();
} else {
document.querySelector("textarea")?.focus();
}
return false;
}
if (arrowKeys.includes(event.key)) {
if (!this._focusedOn(this.elements.picker)) {
return;
}
Array.from(emojis).find((e, index) => {
currentEmoji = index;
return e.isEqualNode(event.target);
});
if (event.key === "ArrowRight") {
let nextEmoji = currentEmoji + 1;
if (nextEmoji < emojis.length) {
emojis[nextEmoji].focus();
} else if (nextEmoji >= emojis.length) {
emojis[0].focus();
}
}
if (event.key === "ArrowLeft") {
const previousEmoji = currentEmoji - 1;
if (currentEmoji > 0) {
emojis[previousEmoji].focus();
}
}
const active = emojis[currentEmoji];
if (event.key === "ArrowDown") {
// source: https://stackoverflow.com/a/49090383/349424
// look for same element type with
// - higher offsetTop
// - same offsetLeft
const emojiBelow = [...emojis]
.filter((c) => c.offsetTop > active.offsetTop)
.find((c) => c.offsetLeft === active.offsetLeft);
emojiBelow?.focus();
}
if (event.key === "ArrowUp") {
// look for same element type with
// - lower offsetTop
// - same offsetLeft
const emojiAbove = [...emojis]
.reverse()
.filter((c) => c.offsetTop < active.offsetTop)
.find((c) => c.offsetLeft === active.offsetLeft);
if (emojiAbove) {
emojiAbove.focus();
} else {
document.querySelector(this.elements.searchInput).focus();
}
}
event.preventDefault();
return false;
}
if (event.key === "Enter") {
if (!this._focusedOn(".emoji")) {
return;
}
this.onEmojiSelection(event);
this.onClose(event);
event.preventDefault();
return false;
}
},
@ -256,6 +368,11 @@ export default Component.extend({
this._applyFilter(event.target.value);
},
_focusedOn(item) {
// returns the item currently being focused on
return document.activeElement.closest(item) ? document.activeElement : null;
},
_applyFilter(filter) {
const emojiPicker = document.querySelector(".emoji-picker");
const results = document.querySelector(".emoji-picker-emoji-area .results");
@ -286,8 +403,9 @@ export default Component.extend({
_replaceEmoji(code) {
const escaped = emojiUnescape(`:${escapeExpression(code)}:`, {
lazy: true,
tabIndex: "0",
});
return htmlSafe(`<span>${escaped}</span>`);
return htmlSafe(escaped);
},
_codeWithDiversity(code, selectedDiversity) {

View File

@ -1,24 +0,0 @@
import GlimmerComponent from "@glimmer/component";
import { inject as service } from "@ember/service";
/*
Glimmer components are not EmberObjects, and therefore do not support automatic
injection of the things defined in `pre-initializers/inject-discourse-objects`.
This base class provides an alternative. All these references are looked up lazily,
so the performance impact should be negligible
*/
export default class DiscourseGlimmerComponent extends GlimmerComponent {
@service appEvents;
@service store;
@service("search") searchService;
@service keyValueStore;
@service pmTopicTrackingState;
@service siteSettings;
@service messageBus;
@service currentUser;
@service session;
@service site;
@service topicTrackingState;
}

View File

@ -77,6 +77,7 @@ export default Component.extend({
@discourseComputed(
"site.isReadOnly",
"site.isStaffWritesOnly",
"siteSettings.login_required",
"siteSettings.disable_emails",
"siteSettings.global_notice",
@ -85,6 +86,7 @@ export default Component.extend({
)
notices(
isReadOnly,
isStaffWritesOnly,
loginRequired,
disableEmails,
globalNotice,
@ -111,7 +113,14 @@ export default Component.extend({
);
}
if (isReadOnly) {
if (isStaffWritesOnly) {
notices.push(
Notice.create({
text: I18n.t("staff_writes_only_mode.enabled"),
id: "alert-staff-writes-only",
})
);
} else if (isReadOnly) {
notices.push(
Notice.create({
text: I18n.t("read_only_mode.enabled"),

View File

@ -2,11 +2,12 @@ import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import I18n from "I18n";
import bootbox from "bootbox";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default Component.extend({
tagName: "",
dialog: service(),
imapSettingsValid: false,
smtpSettingsValid: false,
@ -71,16 +72,11 @@ export default Component.extend({
this.group.smtp_enabled &&
this._anySmtpFieldsFilled()
) {
bootbox.confirm(
I18n.t("groups.manage.email.smtp_disable_confirm"),
(result) => {
if (!result) {
this.group.set("smtp_enabled", true);
} else {
this.group.set("imap_enabled", false);
}
}
);
this.dialog.confirm({
message: I18n.t("groups.manage.email.smtp_disable_confirm"),
didConfirm: () => this.group.set("smtp_enabled", true),
didCancel: () => this.group.set("imap_enabled", false),
});
}
this.group.set("smtp_enabled", event.target.checked);
@ -93,14 +89,10 @@ export default Component.extend({
this.group.imap_enabled &&
this._anyImapFieldsFilled()
) {
bootbox.confirm(
I18n.t("groups.manage.email.imap_disable_confirm"),
(result) => {
if (!result) {
this.group.set("imap_enabled", true);
}
}
);
this.dialog.confirm({
message: I18n.t("groups.manage.email.imap_disable_confirm"),
didConfirm: () => this.group.set("imap_enabled", true),
});
}
this.group.set("imap_enabled", event.target.checked);

View File

@ -1,13 +1,14 @@
import Component from "@ember/component";
import I18n from "I18n";
import bootbox from "bootbox";
import cookie from "discourse/lib/cookie";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import showModal from "discourse/lib/show-modal";
export default Component.extend({
classNames: ["group-membership-button"],
dialog: service(),
@discourseComputed("model.public_admission", "userIsGroupUser")
canJoinGroup(publicAdmission, userIsGroupUser) {
@ -73,16 +74,11 @@ export default Component.extend({
if (this.model.public_admission) {
this.removeFromGroup();
} else {
return bootbox.confirm(
I18n.t("groups.confirm_leave"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
result
? this.removeFromGroup()
: this.set("updatingMembership", false);
}
);
return this.dialog.yesNoConfirm({
message: I18n.t("groups.confirm_leave"),
didConfirm: () => this.removeFromGroup(),
didCancel: () => this.set("updatingMembership", false),
});
}
},

View File

@ -3,6 +3,7 @@ import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import { prioritizeNameInUx } from "discourse/lib/settings";
import { propertyEqual } from "discourse/lib/computed";
import { userPath } from "discourse/lib/url";
export default Component.extend({
classNameBindings: [
@ -35,4 +36,9 @@ export default Component.extend({
return `group-${postUser.primary_group_name}`;
}
},
@discourseComputed("post.user.username")
userUrl(username) {
return userPath(username.toLowerCase());
},
});

View File

@ -42,13 +42,14 @@ export default Component.extend(FilterModeMixin, {
const content = this.content;
let href = content.get("href");
let queryParams = [];
let urlSearchParams = new URLSearchParams();
let addParamsEvenIfEmpty = false;
// Include the category id if the option is present
if (content.get("includeCategoryId")) {
let categoryId = this.get("content.category.id");
if (categoryId) {
queryParams.push(`category_id=${categoryId}`);
urlSearchParams.set("category_id", categoryId);
}
}
@ -56,12 +57,12 @@ export default Component.extend(FilterModeMixin, {
// If no query param is present, add an empty one to ensure a ? is
// appended to the URL.
if (content.currentRouteQueryParams) {
if (content.currentRouteQueryParams.filter && queryParams.length === 0) {
queryParams.push("");
if (content.currentRouteQueryParams.filter) {
addParamsEvenIfEmpty = true;
}
if (content.currentRouteQueryParams.f) {
queryParams.push(`f=${content.currentRouteQueryParams.f}`);
urlSearchParams.set("f", content.currentRouteQueryParams.f);
}
}
@ -69,11 +70,12 @@ export default Component.extend(FilterModeMixin, {
this.siteSettings.desktop_category_page_style ===
"categories_and_latest_topics_created_date"
) {
queryParams.push("order=created");
urlSearchParams.set("order", "created");
}
if (queryParams.length) {
href += `?${queryParams.join("&")}`;
const queryString = urlSearchParams.toString();
if (addParamsEvenIfEmpty || queryString) {
href += `?${queryString}`;
}
this.set("hrefLink", href);

View File

@ -32,7 +32,7 @@ export default DesktopNotificationConfig.extend({
this.siteSettings.push_notifications_prompt &&
!isNotSupported &&
this.currentUser &&
anyPosts &&
(this.capabilities.isPwa || anyPosts) &&
Notification.permission !== "denied" &&
Notification.permission !== "granted" &&
!isEnabled &&

View File

@ -1,5 +1,4 @@
import Component from "@ember/component";
import bootbox from "bootbox";
import { isBlank } from "@ember/utils";
import {
authorizedExtensions,
@ -8,6 +7,7 @@ import {
import { action } from "@ember/object";
import discourseComputed, { bind } from "discourse-common/utils/decorators";
import I18n from "I18n";
import { inject as service } from "@ember/service";
// This picker is intended to be used with UppyUploadMixin or with
// ComposerUploadUppy, which is why there are no change events registered
@ -18,6 +18,7 @@ import I18n from "I18n";
// is sometimes useful if you need to do something outside the uppy upload with
// the file, such as directly using JSON or CSV data from a file in JS.
export default Component.extend({
dialog: service(),
fileInputId: null,
fileInputClass: null,
fileInputDisabled: false,
@ -87,7 +88,7 @@ export default Component.extend({
const message = I18n.t("pick_files_button.unsupported_file_picked", {
types: this.acceptedFileTypesString,
});
bootbox.alert(message);
this.dialog.alert(message);
return;
}
this.onFilesPicked(files);

View File

@ -151,11 +151,11 @@ export default Component.extend({
// "fast track" to update the current user's reviewable count before the message bus finds out.
if (performResult.reviewable_count !== undefined) {
this.currentUser.set(
"reviewable_count",
this.currentUser.updateReviewableCount(
performResult.reviewable_count
);
}
if (performResult.unseen_reviewable_count !== undefined) {
this.currentUser.set(
"unseen_reviewable_count",

View File

@ -0,0 +1,4 @@
<DSection @pageClass="has-sidebar" @class="sidebar-container" @scrollTop={{false}}>
<Sidebar::Sections @currentUser={{this.currentUser}} @collapsableSections={{true}} />
<Sidebar::Footer />
</DSection>

View File

@ -1,15 +1,18 @@
import GlimmerComponent from "discourse/components/glimmer";
import Component from "@glimmer/component";
import { bind } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default class Sidebar extends Component {
@service appEvents;
@service site;
@service currentUser;
export default class Sidebar extends GlimmerComponent {
constructor() {
super(...arguments);
if (this.site.mobileView) {
document.addEventListener("click", this.collapseSidebar);
}
// This appEvent handler is experimental and should not be relied on as an extension point yet.
this.appEvents.on("sidebar:scroll-to-element", this, this.#scrollToElement);
}
@bind
@ -33,68 +36,9 @@ export default class Sidebar extends GlimmerComponent {
}
}
#scrollToElement(destinationElement) {
const topPadding = 10;
const sidebarContainerElement =
document.querySelector(".sidebar-container");
const distanceFromTop =
document.getElementsByClassName(destinationElement)[0].offsetTop -
topPadding;
this.#setMissingHeightForScroll(sidebarContainerElement, distanceFromTop);
sidebarContainerElement.scrollTop = distanceFromTop;
}
#setMissingHeightForScroll(sidebarContainerElement, distanceFromTop) {
const allSections = document.getElementsByClassName(
"sidebar-section-wrapper"
);
const lastSectionElement = allSections[allSections.length - 1];
const lastSectionBottomPadding = parseInt(
lastSectionElement.style.paddingBottom?.replace("px", "") || 0,
10
);
const headerOffset = parseInt(
document.documentElement.style.getPropertyValue("--header-offset"),
10
);
let allSectionsHeight = 0;
for (const section of allSections) {
allSectionsHeight +=
section.clientHeight +
parseInt(
window.getComputedStyle(section).marginBottom.replace("px", ""),
10
);
}
const missingHeight =
sidebarContainerElement.clientHeight -
headerOffset +
lastSectionBottomPadding -
(allSectionsHeight - distanceFromTop);
lastSectionElement.style.paddingBottom =
missingHeight > 0 ? `${missingHeight}px` : null;
}
willDestroy() {
if (this.site.mobileView) {
document.removeEventListener("click", this.collapseSidebar);
}
this.appEvents.off(
"sidebar:scroll-to-element",
this,
this.#scrollToElement
);
}
}

View File

@ -0,0 +1,21 @@
<Sidebar::Section
@sectionName="categories"
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
@collapsable={{@collapsable}} >
{{#each this.sectionLinks as |sectionLink|}}
<Sidebar::SectionLink
@linkName={{sectionLink.name}}
@route={{sectionLink.route}}
@title={{sectionLink.title}}
@content={{sectionLink.text}}
@currentWhen={{sectionLink.currentWhen}}
@model={{sectionLink.model}}
@prefixType={{sectionLink.prefixType}}
@prefixValue={{sectionLink.prefixValue}}
@prefixColor={{sectionLink.prefixColor}} >
</Sidebar::SectionLink>
{{/each}}
<Sidebar::Common::AllCategoriesSectionLink />
</Sidebar::Section>

View File

@ -0,0 +1,31 @@
import { inject as service } from "@ember/service";
import { canDisplayCategory } from "discourse/lib/sidebar/helpers";
import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section";
export default class SidebarAnonymousCategoriesSection extends SidebarCommonCategoriesSection {
@service site;
get categories() {
let categories = this.site.categoriesList;
if (this.siteSettings.default_sidebar_categories) {
const defaultCategoryIds = this.siteSettings.default_sidebar_categories
.split("|")
.map((categoryId) => parseInt(categoryId, 10));
categories = categories.filter((category) =>
defaultCategoryIds.includes(category.id)
);
} else {
categories = categories
.filter(
(category) =>
canDisplayCategory(category, this.siteSettings) &&
!category.parent_category_id
)
.slice(0, 5);
}
return categories;
}
}

View File

@ -0,0 +1,36 @@
import SidebarCommonCommunitySection from "discourse/components/sidebar/common/community-section";
import EverythingSectionLink from "discourse/lib/sidebar/common/community-section/everything-section-link";
import AboutSectionLink from "discourse/lib/sidebar/common/community-section/about-section-link";
import FAQSectionLink from "discourse/lib/sidebar/common/community-section/faq-section-link";
import GroupsSectionLink from "discourse/lib/sidebar/common/community-section/groups-section-link";
import UsersSectionLink from "discourse/lib/sidebar/common/community-section/users-section-link";
import BadgesSectionLink from "discourse/lib/sidebar/common/community-section/badges-section-link";
export default class SidebarAnonymousCommunitySection extends SidebarCommonCommunitySection {
get defaultMainSectionLinks() {
const defaultLinks = [
EverythingSectionLink,
UsersSectionLink,
FAQSectionLink,
];
defaultLinks.splice(
this.displayShortSiteDescription ? 0 : 2,
0,
AboutSectionLink
);
return defaultLinks;
}
get displayShortSiteDescription() {
return (
!this.currentUser &&
(this.siteSettings.short_site_description || "").length > 0
);
}
get defaultMoreSectionLinks() {
return [GroupsSectionLink, BadgesSectionLink];
}
}

View File

@ -0,0 +1,8 @@
<div class="sidebar-sections sidebar-sections-anonymous">
<Sidebar::Anonymous::CommunitySection @collapsable={{@collapsableSections}} />
<Sidebar::Anonymous::CategoriesSection @collapsable={{@collapsableSections}} />
{{#if this.siteSettings.tagging_enabled}}
<Sidebar::Anonymous::TagsSection @collapsable={{@collapsableSections}} />
{{/if}}
</div>

View File

@ -0,0 +1,6 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class SidebarAnonymousSections extends Component {
@service siteSettings;
}

View File

@ -0,0 +1,18 @@
<Sidebar::Section
@sectionName="tags"
@headerLinkText={{i18n "sidebar.sections.tags.header_link_text"}}
@collapsable={{@collapsable}} >
{{#each this.sectionLinks as |sectionLink|}}
<Sidebar::SectionLink
@route={{sectionLink.route}}
@content={{sectionLink.text}}
@currentWhen={{sectionLink.currentWhen}}
@prefixType={{sectionLink.prefixType}}
@prefixValue={{sectionLink.prefixValue}}
@models={{sectionLink.models}} >
</Sidebar::SectionLink>
{{/each}}
<Sidebar::Common::AllTagsSectionLink />
</Sidebar::Section>

View File

@ -0,0 +1,27 @@
import { cached } from "@glimmer/tracking";
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import TagSectionLink from "discourse/lib/sidebar/user/tags-section/tag-section-link";
export default class SidebarAnonymousTagsSection extends Component {
@service router;
@service topicTrackingState;
@service site;
@cached
get sectionLinks() {
let tags;
if (this.site.anonymous_default_sidebar_tags) {
tags = this.site.anonymous_default_sidebar_tags;
} else {
tags = this.site.top_tags.slice(0, 5);
}
return tags.map((tagName) => {
return new TagSectionLink({
tagName,
topicTrackingState: this.topicTrackingState,
});
});
}
}

View File

@ -0,0 +1,7 @@
<Sidebar::SectionLink
@linkName="all-categories"
@content={{i18n "sidebar.all_categories"}}
@route="discovery.categories"
@prefixType="icon"
@prefixValue="list"
/>

View File

@ -0,0 +1,3 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -0,0 +1,7 @@
<Sidebar::SectionLink
@linkName="all-tags"
@content={{i18n "sidebar.all_tags"}}
@route="tags"
@prefixType="icon"
@prefixValue="list"
/>

View File

@ -0,0 +1,3 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -0,0 +1,29 @@
import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
import CategorySectionLink from "discourse/lib/sidebar/user/categories-section/category-section-link";
export default class SidebarCommonCategoriesSection extends Component {
@service topicTrackingState;
@service siteSettings;
// Override in child
get categories() {}
@cached
get sectionLinks() {
return this.categories
.sort((a, b) => a.name.localeCompare(b.name))
.reduce((links, category) => {
links.push(
new CategorySectionLink({
category,
topicTrackingState: this.topicTrackingState,
})
);
return links;
}, []);
}
}

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