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:
commit
d4adf6fa66
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
88
.github/dependabot.yml
vendored
88
.github/dependabot.yml
vendored
@ -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
|
||||
|
||||
6
.github/workflows/licenses.yml
vendored
6
.github/workflows/licenses.yml
vendored
@ -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
|
||||
|
||||
14
.github/workflows/linting.yml
vendored
14
.github/workflows/linting.yml
vendored
@ -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"
|
||||
|
||||
83
.github/workflows/tests.yml
vendored
83
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
@ -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/
|
||||
|
||||
5
Gemfile
5
Gemfile
@ -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'
|
||||
|
||||
186
Gemfile.lock
186
Gemfile.lock
@ -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
|
||||
|
||||
@ -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 [W3C’s 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 [W3C’s 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
|
||||
|
||||
|
||||
@ -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
|
||||
%>
|
||||
@ -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}`,
|
||||
})
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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"));
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -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"));
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@ -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"))
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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],
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
})
|
||||
|
||||
@ -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", []);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"))
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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),
|
||||
})
|
||||
|
||||
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<div class="composer-fullscreen-prompt" {{on "animationend" @removeFullScreenExitPrompt}}>
|
||||
{{html-safe (i18n "composer.exit_fullscreen_prompt")}}
|
||||
</div>
|
||||
@ -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" />
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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' +
|
||||
|
||||
110
app/assets/javascripts/discourse-js-processor.js
Normal file
110
app/assets/javascripts/discourse-js-processor.js
Normal 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;
|
||||
}
|
||||
};
|
||||
213
app/assets/javascripts/discourse-plugins/index.js
Normal file
213
app/assets/javascripts/discourse-plugins/index.js
Normal 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;
|
||||
},
|
||||
};
|
||||
25
app/assets/javascripts/discourse-plugins/package.json
Normal file
25
app/assets/javascripts/discourse-plugins/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -816,7 +816,6 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
|
||||
extraButtons(toolbar) {
|
||||
toolbar.addButton({
|
||||
tabindex: "0",
|
||||
id: "quote",
|
||||
group: "fontStyles",
|
||||
icon: "far-comment",
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import templateOnly from "@ember/component/template-only";
|
||||
|
||||
export default templateOnly();
|
||||
@ -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"),
|
||||
});
|
||||
|
||||
@ -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")
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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"),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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());
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 &&
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
<DSection @pageClass="has-sidebar" @class="sidebar-container" @scrollTop={{false}}>
|
||||
<Sidebar::Sections @currentUser={{this.currentUser}} @collapsableSections={{true}} />
|
||||
<Sidebar::Footer />
|
||||
</DSection>
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1,6 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class SidebarAnonymousSections extends Component {
|
||||
@service siteSettings;
|
||||
}
|
||||
@ -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>
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
<Sidebar::SectionLink
|
||||
@linkName="all-categories"
|
||||
@content={{i18n "sidebar.all_categories"}}
|
||||
@route="discovery.categories"
|
||||
@prefixType="icon"
|
||||
@prefixValue="list"
|
||||
/>
|
||||
@ -0,0 +1,3 @@
|
||||
import templateOnly from "@ember/component/template-only";
|
||||
|
||||
export default templateOnly();
|
||||
@ -0,0 +1,7 @@
|
||||
<Sidebar::SectionLink
|
||||
@linkName="all-tags"
|
||||
@content={{i18n "sidebar.all_tags"}}
|
||||
@route="tags"
|
||||
@prefixType="icon"
|
||||
@prefixValue="list"
|
||||
/>
|
||||
@ -0,0 +1,3 @@
|
||||
import templateOnly from "@ember/component/template-only";
|
||||
|
||||
export default templateOnly();
|
||||
@ -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
Reference in New Issue
Block a user