Merge master
@ -8,10 +8,9 @@ lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.js
|
||||
lib/highlight_js/
|
||||
plugins/**/lib/javascripts/locale
|
||||
public/javascripts/
|
||||
public/
|
||||
vendor/
|
||||
test/javascripts/test_helper.js
|
||||
test/javascripts/fixtures
|
||||
test/javascripts/helpers/assertions.js
|
||||
app/assets/javascripts/discourse/tests/test_helper.js
|
||||
app/assets/javascripts/discourse/tests/fixtures
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
15
.eslintrc
@ -1,7 +1,18 @@
|
||||
{
|
||||
"extends": "eslint-config-discourse",
|
||||
"plugins": ["discourse-ember"],
|
||||
"rules": {
|
||||
"discourse-ember/global-ember": 2
|
||||
"discourse-ember/global-ember": 2,
|
||||
"no-duplicate-imports": 2
|
||||
},
|
||||
"globals": {
|
||||
"moduleFor": "off",
|
||||
"moduleForComponent": "off",
|
||||
"testStart": "off",
|
||||
"testDone": "off",
|
||||
"sinon": "off",
|
||||
"currentURL": "off",
|
||||
"invisible": "off",
|
||||
"visible": "off",
|
||||
"count": "off",
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,3 +28,27 @@ c4644c61d97c823b7dd940ffaf0967a104f4b58c
|
||||
|
||||
# DEV: Fix indentation for routes.rb
|
||||
985900818ff985b04def6aa4c5d99c1aa6dbd45c
|
||||
|
||||
# Add rubocop to our build.
|
||||
5012d46cbd3bcf79b7351f7d2d41003496a796c5
|
||||
|
||||
# Make rubocop happy again.
|
||||
ad5082d969ab1f60b5c5b1e89a616117906289f8
|
||||
|
||||
# DEV: apply new coding standards (#10592)
|
||||
52672b9eabccb1184d85dc7f08062d5a7c18cb73
|
||||
|
||||
# DEV: apply coding standards to plugins (#10594)
|
||||
bf88410126f73aab47b7e694e3c5b46453cec1b6
|
||||
|
||||
# REFACTOR: Support bundling our `admin` section as an ember addon
|
||||
ce3fe2f4c4ddf166949ee3cec3d9ecbf9108ab52
|
||||
|
||||
# REFACTOR: Move qunit tests to a different directory structure
|
||||
bc97c79a35d8acd283d4d8b79aa079bce9d127c6
|
||||
|
||||
# REFACTOR: Move javascript tests inside discourse app
|
||||
23f24bfb510edb25b18b6a0d5485270c88df9b24
|
||||
|
||||
# DEV: Tidy up imports. (#11364)
|
||||
1c2358ba162eb9f9ba9095c9afe30cf51dd85e04
|
||||
|
||||
38
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
version: 2
|
||||
updates:
|
||||
- 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"
|
||||
1
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in Javascript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
|
||||
62
.github/workflows/ci.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
||||
if: env.BUILD_TYPE != 'LINT'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead
|
||||
sudo apt-get -yqq install postgresql-client libpq-dev jpegoptim optipng jhead
|
||||
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
|
||||
|
||||
- name: Update imagemagick
|
||||
@ -91,9 +91,10 @@ jobs:
|
||||
gem install bundler -v 2.1.4 --no-doc
|
||||
bundle config deployment 'true'
|
||||
bundle config without 'development'
|
||||
bundle config path vendor/bundle
|
||||
|
||||
- name: Bundler cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
id: bundler-cache
|
||||
with:
|
||||
path: vendor/bundle
|
||||
@ -109,7 +110,7 @@ jobs:
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir.outputs.dir }}
|
||||
@ -136,21 +137,56 @@ jobs:
|
||||
bin/rake parallel:create
|
||||
bin/rake parallel:migrate
|
||||
|
||||
- name: Rubocop
|
||||
if: env.BUILD_TYPE == 'LINT'
|
||||
- name: Rubocop (core and core plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
|
||||
run: bundle exec rubocop .
|
||||
|
||||
- name: ESLint
|
||||
if: env.BUILD_TYPE == 'LINT'
|
||||
run: |
|
||||
yarn eslint app/assets/javascripts test/javascripts
|
||||
yarn eslint --global I18n --ext .es6 plugins
|
||||
- name: Rubocop (all plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
|
||||
run: bundle exec rubocop plugins
|
||||
|
||||
- name: Prettier
|
||||
if: env.BUILD_TYPE == 'LINT'
|
||||
- name: ESLint (core)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
|
||||
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern app/assets/javascripts
|
||||
|
||||
- name: ESLint (core plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
|
||||
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern plugins/**/{test,assets}/javascripts
|
||||
|
||||
- name: ESLint (all plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
|
||||
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern plugins/**/{test,assets}/javascripts
|
||||
|
||||
- name: Prettier (core and core plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
|
||||
run: |
|
||||
yarn prettier -v
|
||||
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
|
||||
yarn prettier --list-different \
|
||||
"app/assets/stylesheets/**/*.scss" \
|
||||
"app/assets/javascripts/**/*.{js,es6}" \
|
||||
"plugins/**/assets/stylesheets/**/*.scss" \
|
||||
"plugins/**/assets/javascripts/**/*.{js,es6}"
|
||||
|
||||
- name: Prettier (all plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
|
||||
run: |
|
||||
yarn prettier -v
|
||||
yarn prettier --list-different \
|
||||
"plugins/**/assets/stylesheets/**/*.scss" \
|
||||
"plugins/**/assets/javascripts/**/*.{js,es6}"
|
||||
|
||||
- name: Ember template lint (core and core plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
|
||||
run: |
|
||||
yarn ember-template-lint \
|
||||
app/assets/javascripts \
|
||||
plugins/**/assets/javascripts
|
||||
|
||||
- name: Ember template lint (all plugins)
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
|
||||
run: |
|
||||
yarn ember-template-lint \
|
||||
plugins/**/assets/javascripts
|
||||
|
||||
- name: Core English locale
|
||||
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
|
||||
|
||||
11
.gitignore
vendored
@ -53,8 +53,8 @@ bootsnap-compile-cache/
|
||||
!/plugins/discourse-nginx-performance-report
|
||||
!/plugins/discourse-narrative-bot
|
||||
!/plugins/discourse-presence
|
||||
!/plugins/styleguide
|
||||
!/plugins/discourse-local-dates
|
||||
!/plugins/discourse-unsupported-browser
|
||||
/plugins/*/auto_generated/
|
||||
|
||||
/spec/fixtures/plugins/my_plugin/auto_generated
|
||||
@ -88,6 +88,7 @@ config/multisite.yml
|
||||
config/multisite1.yml
|
||||
config/fog_credentials.yml
|
||||
|
||||
/public/fonts
|
||||
/public/uploads
|
||||
/public/backups
|
||||
/public/stylesheet-cache/*
|
||||
@ -136,8 +137,16 @@ node_modules
|
||||
# ignore generated api documentation files
|
||||
openapi/*
|
||||
|
||||
# ignore VSCode config files
|
||||
.vscode
|
||||
|
||||
# ignore direnv
|
||||
.envrc
|
||||
|
||||
# ember-cli generated
|
||||
dist
|
||||
|
||||
# Copyright Deposits
|
||||
copyright
|
||||
|
||||
yarn-error.log
|
||||
|
||||
12
.licensed.yml
Normal file
@ -0,0 +1,12 @@
|
||||
sources:
|
||||
yarn: true
|
||||
bundler: true
|
||||
allowed:
|
||||
- mit
|
||||
- apache-2.0
|
||||
- bsd-2-clause
|
||||
- bsd-3-clause
|
||||
- cc0-1.0
|
||||
- isc
|
||||
- other
|
||||
- none
|
||||
@ -1,6 +1,25 @@
|
||||
app/assets/stylesheets/vendor/
|
||||
plugins/**/assets/stylesheets/vendor/
|
||||
plugins/**/assets/javascripts/vendor/
|
||||
package.json
|
||||
config/locales/**/*.yml
|
||||
!config/locales/**/*.en*.yml
|
||||
script/import_scripts/**/*.yml
|
||||
|
||||
app/assets/javascripts/env.js
|
||||
app/assets/javascripts/main_include_admin.js
|
||||
app/assets/javascripts/vendor.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
app/assets/javascripts/discourse/lib/autosize.js
|
||||
lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.js
|
||||
lib/highlight_js/
|
||||
plugins/**/lib/javascripts/locale
|
||||
public/
|
||||
vendor/
|
||||
app/assets/javascripts/discourse/tests/test_helper.js
|
||||
app/assets/javascripts/discourse/tests/fixtures
|
||||
node_modules/
|
||||
dist/
|
||||
**/*.rb
|
||||
|
||||
1
.prettierrc
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -50,6 +50,6 @@ module.exports = {
|
||||
"simple-unless": true,
|
||||
"style-concatenation": true,
|
||||
"table-groups": true,
|
||||
"link-href-attributes": false
|
||||
}
|
||||
"link-href-attributes": false,
|
||||
},
|
||||
};
|
||||
|
||||
99
.tx/config
@ -1,99 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = el_GR: el, es_ES: es, fr_FR: fr, hu_HU: hu, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
|
||||
|
||||
[discourse-org.core-client-yml]
|
||||
file_filter = config/locales/client.<lang>.yml
|
||||
source_file = config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.core-server-yml]
|
||||
file_filter = config/locales/server.<lang>.yml
|
||||
source_file = config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.pollclientenyml]
|
||||
file_filter = plugins/poll/config/locales/client.<lang>.yml
|
||||
source_file = plugins/poll/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.pollserverenyml]
|
||||
file_filter = plugins/poll/config/locales/server.<lang>.yml
|
||||
source_file = plugins/poll/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.narrativeclientenyml]
|
||||
file_filter = plugins/discourse-narrative-bot/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-narrative-bot/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.narrativeserverenyml]
|
||||
file_filter = plugins/discourse-narrative-bot/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-narrative-bot/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.discourse-presenceclientenyml]
|
||||
file_filter = plugins/discourse-presence/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-presence/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.discourse-presenceserverenyml]
|
||||
file_filter = plugins/discourse-presence/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-presence/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.coreplugindetailsclientyml]
|
||||
file_filter = plugins/discourse-details/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-details/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.coreplugindetailsserveryml]
|
||||
file_filter = plugins/discourse-details/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-details/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.core-plugin-local-dates-client-yml]
|
||||
file_filter = plugins/discourse-local-dates/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-local-dates/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.core-plugin-local-dates-server-yml]
|
||||
file_filter = plugins/discourse-local-dates/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-local-dates/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.403html]
|
||||
file_filter = public/403.<lang>.html
|
||||
source_file = public/403.html
|
||||
source_lang = en
|
||||
type = HTML
|
||||
|
||||
[discourse-org.422html]
|
||||
file_filter = public/422.<lang>.html
|
||||
source_file = public/422.html
|
||||
source_lang = en
|
||||
type = HTML
|
||||
|
||||
[discourse-org.500html]
|
||||
file_filter = public/500.<lang>.html
|
||||
source_file = public/500.html
|
||||
source_lang = en
|
||||
type = HTML
|
||||
|
||||
[discourse-org.503html]
|
||||
file_filter = public/503.<lang>.html
|
||||
source_file = public/503.html
|
||||
source_lang = en
|
||||
type = HTML
|
||||
@ -17,7 +17,7 @@ locales_changes = git.modified_files.grep(%r{config/locales})
|
||||
has_non_en_locales_changes = locales_changes.grep_v(%r{config/locales/(?:client|server)\.(?:en|en_US)\.yml}).any?
|
||||
|
||||
if locales_changes.any? && has_non_en_locales_changes
|
||||
fail("Please submit your non-English translation updates via [Transifex](https://www.transifex.com/discourse/discourse-org/). You can read more on how to contribute translations [here](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882).")
|
||||
fail("Please submit your non-English translation updates via [Crowdin](https://translate.discourse.org/). You can read more on how to contribute translations [here](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882).")
|
||||
end
|
||||
|
||||
files = (git.added_files + git.modified_files)
|
||||
|
||||
39
Gemfile
@ -18,13 +18,13 @@ else
|
||||
# this allows us to include the bits of rails we use without pieces we do not.
|
||||
#
|
||||
# To issue a rails update bump the version number here
|
||||
gem 'actionmailer', '6.0.3.1'
|
||||
gem 'actionpack', '6.0.3.1'
|
||||
gem 'actionview', '6.0.3.1'
|
||||
gem 'activemodel', '6.0.3.1'
|
||||
gem 'activerecord', '6.0.3.1'
|
||||
gem 'activesupport', '6.0.3.1'
|
||||
gem 'railties', '6.0.3.1'
|
||||
gem 'actionmailer', '6.0.3.3'
|
||||
gem 'actionpack', '6.0.3.3'
|
||||
gem 'actionview', '6.0.3.3'
|
||||
gem 'activemodel', '6.0.3.3'
|
||||
gem 'activerecord', '6.0.3.3'
|
||||
gem 'activesupport', '6.0.3.3'
|
||||
gem 'railties', '6.0.3.3'
|
||||
gem 'sprockets-rails'
|
||||
end
|
||||
|
||||
@ -66,9 +66,10 @@ gem 'http_accept_language', require: false
|
||||
|
||||
# Ember related gems need to be pinned cause they control client side
|
||||
# behavior, we will push these versions up when upgrading ember
|
||||
gem 'ember-rails', '0.18.5'
|
||||
gem 'discourse-ember-rails', '0.18.6', require: 'ember-rails'
|
||||
gem 'discourse-ember-source', '~> 3.12.2'
|
||||
gem 'ember-handlebars-template', '0.8.0'
|
||||
gem 'discourse-fonts'
|
||||
|
||||
gem 'barber'
|
||||
|
||||
@ -76,10 +77,9 @@ gem 'message_bus'
|
||||
|
||||
gem 'rails_multisite'
|
||||
|
||||
gem 'fast_xs', platform: :mri
|
||||
gem 'fast_xs', platform: :ruby
|
||||
|
||||
# may move to xorcist post: https://github.com/fny/xorcist/issues/4
|
||||
gem 'fast_xor', platform: :mri
|
||||
gem 'xorcist'
|
||||
|
||||
gem 'fastimage'
|
||||
|
||||
@ -101,7 +101,6 @@ gem 'css_parser', require: false
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-facebook'
|
||||
gem 'omniauth-twitter'
|
||||
gem 'omniauth-instagram'
|
||||
gem 'omniauth-github'
|
||||
|
||||
gem 'omniauth-oauth2', require: false
|
||||
@ -125,10 +124,9 @@ gem 'mini_scheduler'
|
||||
gem 'execjs', require: false
|
||||
gem 'mini_racer'
|
||||
|
||||
# TODO: determine why highline is being held back and upgrade to latest
|
||||
gem 'highline', '~> 1.7.0', require: false
|
||||
gem 'highline', require: false
|
||||
|
||||
gem 'rack', '2.2.2'
|
||||
gem 'rack'
|
||||
|
||||
gem 'rack-protection' # security
|
||||
gem 'cbor', require: false
|
||||
@ -160,10 +158,6 @@ group :test, :development do
|
||||
|
||||
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
|
||||
|
||||
# TODO determine if we can update this to 0.10, API changes happened
|
||||
# we would like to upgrade it if possible
|
||||
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
|
||||
|
||||
gem 'rspec-rails'
|
||||
|
||||
gem 'shoulda-matchers', require: false
|
||||
@ -187,7 +181,7 @@ end
|
||||
# this is an optional gem, it provides a high performance replacement
|
||||
# to String#blank? a method that is called quite frequently in current
|
||||
# ActiveRecord, this may change in the future
|
||||
gem 'fast_blank', platform: :mri
|
||||
gem 'fast_blank', platform: :ruby
|
||||
|
||||
# this provides a very efficient lru cache
|
||||
gem 'lru_redux'
|
||||
@ -201,7 +195,7 @@ gem 'htmlentities', require: false
|
||||
gem 'flamegraph', require: false
|
||||
gem 'rack-mini-profiler', require: ['enable_rails_patches']
|
||||
|
||||
gem 'unicorn', require: false, platform: :mri
|
||||
gem 'unicorn', require: false, platform: :ruby
|
||||
gem 'puma', require: false
|
||||
gem 'rbtrace', require: false, platform: :mri
|
||||
gem 'gc_tracer', require: false, platform: :mri
|
||||
@ -225,6 +219,7 @@ gem 'sassc', '2.0.1', require: false
|
||||
gem "sassc-rails"
|
||||
|
||||
gem 'rotp', require: false
|
||||
|
||||
gem 'rqrcode'
|
||||
|
||||
gem 'rubyzip', require: false
|
||||
@ -232,7 +227,7 @@ gem 'rubyzip', require: false
|
||||
gem 'sshkey', require: false
|
||||
|
||||
gem 'rchardet', require: false
|
||||
gem 'lz4-ruby', require: false, platform: :mri
|
||||
gem 'lz4-ruby', require: false, platform: :ruby
|
||||
|
||||
if ENV["IMPORT"] == "1"
|
||||
gem 'mysql2'
|
||||
|
||||
295
Gemfile.lock
@ -1,38 +1,38 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
actionview (= 6.0.3.1)
|
||||
activejob (= 6.0.3.1)
|
||||
actionmailer (6.0.3.3)
|
||||
actionpack (= 6.0.3.3)
|
||||
actionview (= 6.0.3.3)
|
||||
activejob (= 6.0.3.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.1)
|
||||
actionview (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
actionpack (6.0.3.3)
|
||||
actionview (= 6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actionview (6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
actionview (6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
actionview_precompiler (0.2.2)
|
||||
actionview_precompiler (0.2.3)
|
||||
actionview (>= 6.0.a)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
activejob (6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
activejob (6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
activerecord (6.0.3.1)
|
||||
activemodel (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
activesupport (6.0.3.1)
|
||||
activemodel (6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
activerecord (6.0.3.3)
|
||||
activemodel (= 6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
activesupport (6.0.3.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
@ -45,34 +45,34 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.1)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.329.0)
|
||||
aws-sdk-core (3.99.1)
|
||||
aws-partitions (1.390.0)
|
||||
aws-sdk-core (3.109.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.31.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sdk-kms (1.39.0)
|
||||
aws-sdk-core (~> 3, >= 3.109.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.66.0)
|
||||
aws-sdk-core (~> 3, >= 3.96.1)
|
||||
aws-sdk-s3 (1.83.2)
|
||||
aws-sdk-core (~> 3, >= 3.109.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-sns (1.25.1)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sdk-sns (1.35.0)
|
||||
aws-sdk-core (~> 3, >= 3.109.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.0)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
barber (0.12.2)
|
||||
ember-source (>= 1.0, < 3.1)
|
||||
execjs (>= 1.2, < 3)
|
||||
better_errors (2.7.1)
|
||||
better_errors (2.9.1)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.4.6)
|
||||
bootsnap (1.5.1)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
@ -81,24 +81,31 @@ GEM
|
||||
byebug (11.1.3)
|
||||
cbor (0.5.9.6)
|
||||
certified (1.0.0)
|
||||
chunky_png (1.3.11)
|
||||
chunky_png (1.3.14)
|
||||
coderay (1.1.3)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.6)
|
||||
concurrent-ruby (1.1.7)
|
||||
connection_pool (2.2.3)
|
||||
cose (1.0.0)
|
||||
cose (1.2.0)
|
||||
cbor (~> 0.5.9)
|
||||
openssl-signature_algorithm (~> 0.4.0)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
cppjieba_rb (0.3.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crack (0.4.4)
|
||||
crass (1.0.6)
|
||||
css_parser (1.7.1)
|
||||
addressable
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.4.1)
|
||||
diffy (3.3.0)
|
||||
discourse-ember-source (3.12.2.0)
|
||||
diff-lcs (1.4.4)
|
||||
diffy (3.4.0)
|
||||
discourse-ember-rails (0.18.6)
|
||||
active_model_serializers
|
||||
ember-data-source (>= 1.0.0.beta.5)
|
||||
ember-handlebars-template (>= 0.1.1, < 1.0)
|
||||
ember-source (>= 1.1.0)
|
||||
jquery-rails (>= 1.0.17)
|
||||
railties (>= 3.1)
|
||||
discourse-ember-source (3.12.2.2)
|
||||
discourse-fonts (0.0.5)
|
||||
discourse_image_optim (0.26.2)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
@ -112,28 +119,19 @@ GEM
|
||||
ember-handlebars-template (0.8.0)
|
||||
barber (>= 0.11.0)
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-rails (0.18.5)
|
||||
active_model_serializers
|
||||
ember-data-source (>= 1.0.0.beta.5)
|
||||
ember-handlebars-template (>= 0.1.1, < 1.0)
|
||||
ember-source (>= 1.1.0)
|
||||
jquery-rails (>= 1.0.17)
|
||||
railties (>= 3.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.9.0)
|
||||
excon (0.75.0)
|
||||
erubi (1.10.0)
|
||||
excon (0.78.0)
|
||||
execjs (2.7.0)
|
||||
exifr (1.3.6)
|
||||
exifr (1.3.9)
|
||||
fabrication (2.21.1)
|
||||
fakeweb (1.3.0)
|
||||
faraday (1.0.1)
|
||||
faraday (1.1.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords
|
||||
fast_blank (1.0.0)
|
||||
fast_xor (1.1.3)
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
fastimage (2.1.7)
|
||||
fastimage (2.2.0)
|
||||
ffi (1.13.1)
|
||||
flamegraph (0.9.5)
|
||||
fspath (3.1.2)
|
||||
@ -143,11 +141,11 @@ GEM
|
||||
guess_html_encoding (0.0.11)
|
||||
hashdiff (1.0.1)
|
||||
hashie (4.1.0)
|
||||
highline (1.7.10)
|
||||
highline (2.0.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http_accept_language (2.1.1)
|
||||
i18n (1.8.3)
|
||||
i18n (1.8.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_size (1.5.0)
|
||||
in_threads (1.5.4)
|
||||
@ -156,13 +154,13 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.0)
|
||||
json (2.3.1)
|
||||
json-schema (2.8.1)
|
||||
addressable (>= 2.4)
|
||||
jwt (2.2.1)
|
||||
jwt (2.2.2)
|
||||
kgio (2.11.3)
|
||||
libv8 (8.4.255.0)
|
||||
listen (3.2.1)
|
||||
listen (3.3.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
lograge (0.11.2)
|
||||
@ -173,8 +171,8 @@ GEM
|
||||
logstash-event (1.2.02)
|
||||
logstash-logger (0.26.1)
|
||||
logstash-event (~> 1.2)
|
||||
logster (2.9.0)
|
||||
loofah (2.6.0)
|
||||
logster (2.9.4)
|
||||
loofah (2.8.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lru_redux (1.1.0)
|
||||
@ -183,28 +181,28 @@ GEM
|
||||
mini_mime (>= 0.1.1)
|
||||
maxminddb (0.1.22)
|
||||
memory_profiler (0.9.14)
|
||||
message_bus (3.3.1)
|
||||
message_bus (3.3.4)
|
||||
rack (>= 1.1.3)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
mini_racer (0.3.1)
|
||||
libv8 (~> 8.4.255)
|
||||
mini_scheduler (0.12.2)
|
||||
mini_scheduler (0.12.3)
|
||||
sidekiq
|
||||
mini_sql (0.2.5)
|
||||
mini_sql (0.3)
|
||||
mini_suffix (0.3.0)
|
||||
ffi (~> 1.9)
|
||||
minitest (5.14.1)
|
||||
minitest (5.14.2)
|
||||
mocha (1.11.2)
|
||||
mock_redis (0.24.0)
|
||||
mock_redis (0.26.0)
|
||||
msgpack (1.3.3)
|
||||
multi_json (1.14.1)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
mustache (1.1.1)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.9)
|
||||
nio4r (2.5.4)
|
||||
nokogiri (1.10.10)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.2)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
@ -215,11 +213,11 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oj (3.10.6)
|
||||
oj (3.10.16)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-facebook (6.0.0)
|
||||
omniauth-facebook (8.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.4.0)
|
||||
omniauth (~> 1.5)
|
||||
@ -228,31 +226,28 @@ GEM
|
||||
jwt (>= 2.0)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.6)
|
||||
omniauth-instagram (1.3.0)
|
||||
omniauth (~> 1)
|
||||
omniauth-oauth2 (~> 1)
|
||||
omniauth-oauth (1.1.0)
|
||||
oauth
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (1.6.0)
|
||||
oauth2 (~> 1.1)
|
||||
omniauth-oauth2 (1.7.0)
|
||||
oauth2 (~> 1.4)
|
||||
omniauth (~> 1.9)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.9.29)
|
||||
onebox (2.1.7)
|
||||
addressable (~> 2.7.0)
|
||||
htmlentities (~> 4.3)
|
||||
multi_json (~> 1.11)
|
||||
mustache
|
||||
nokogiri (~> 1.7)
|
||||
sanitize
|
||||
openssl-signature_algorithm (0.4.0)
|
||||
openssl-signature_algorithm (1.0.0)
|
||||
optimist (3.0.1)
|
||||
parallel (1.19.2)
|
||||
parallel_tests (3.0.0)
|
||||
parallel (1.20.1)
|
||||
parallel_tests (3.4.0)
|
||||
parallel
|
||||
parser (2.7.1.4)
|
||||
parser (2.7.2.0)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
progress (3.5.2)
|
||||
@ -264,14 +259,14 @@ GEM
|
||||
pry (~> 0.13.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.5)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.0.4)
|
||||
nio4r (~> 2.0)
|
||||
r2 (0.2.7)
|
||||
rack (2.2.2)
|
||||
rack-mini-profiler (2.0.2)
|
||||
rack (2.2.3)
|
||||
rack-mini-profiler (2.2.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (2.0.8.1)
|
||||
rack-protection (2.1.0)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
@ -280,60 +275,58 @@ GEM
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails_failover (0.5.5)
|
||||
rails_failover (0.6.2)
|
||||
activerecord (~> 6.0)
|
||||
concurrent-ruby
|
||||
railties (~> 6.0)
|
||||
rails_multisite (2.3.0)
|
||||
rails_multisite (2.5.0)
|
||||
activerecord (> 5.0, < 7)
|
||||
railties (> 5.0, < 7)
|
||||
railties (6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
railties (6.0.3.3)
|
||||
actionpack (= 6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
raindrops (0.19.1)
|
||||
rake (13.0.1)
|
||||
rake-compiler (1.1.0)
|
||||
rake
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rbtrace (0.4.13)
|
||||
rbtrace (0.4.14)
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
optimist (>= 3.0.0)
|
||||
rchardet (1.8.0)
|
||||
redis (4.2.1)
|
||||
redis-namespace (1.7.0)
|
||||
redis (4.2.5)
|
||||
redis-namespace (1.8.0)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (1.7.1)
|
||||
regexp_parser (2.0.0)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
rexml (3.2.4)
|
||||
rinku (2.0.6)
|
||||
rotp (5.1.0)
|
||||
addressable (~> 2.5)
|
||||
rotp (6.2.0)
|
||||
rqrcode (1.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.2)
|
||||
rspec (3.9.0)
|
||||
rspec-core (~> 3.9.0)
|
||||
rspec-expectations (~> 3.9.0)
|
||||
rspec-mocks (~> 3.9.0)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-html-matchers (0.9.2)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-html-matchers (0.9.4)
|
||||
nokogiri (~> 1)
|
||||
rspec (>= 3.0.0.a, < 4)
|
||||
rspec-mocks (3.9.1)
|
||||
rspec-mocks (3.10.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
@ -342,35 +335,36 @@ GEM
|
||||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.3)
|
||||
rspec-support (3.10.0)
|
||||
rswag-specs (2.3.1)
|
||||
activesupport (>= 3.1, < 7.0)
|
||||
json-schema (~> 2.2)
|
||||
railties (>= 3.1, < 7.0)
|
||||
rtlit (0.0.5)
|
||||
rubocop (0.86.0)
|
||||
rubocop (1.4.2)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.0.1)
|
||||
parser (>= 2.7.1.5)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
regexp_parser (>= 1.8)
|
||||
rexml
|
||||
rubocop-ast (>= 0.0.3, < 1.0)
|
||||
rubocop-ast (>= 1.1.1)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.0.3)
|
||||
parser (>= 2.7.0.1)
|
||||
rubocop-discourse (2.2.0)
|
||||
rubocop (>= 0.69.0)
|
||||
rubocop-rspec (>= 1.39.0)
|
||||
rubocop-rspec (1.40.0)
|
||||
rubocop (>= 0.68.1)
|
||||
ruby-prof (1.4.1)
|
||||
rubocop-ast (1.2.0)
|
||||
parser (>= 2.7.1.5)
|
||||
rubocop-discourse (2.4.1)
|
||||
rubocop (>= 1.1.0)
|
||||
rubocop-rspec (>= 2.0.0)
|
||||
rubocop-rspec (2.0.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
ruby-prof (1.4.2)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
nokogiri (>= 1.6.0)
|
||||
ruby2_keywords (0.0.2)
|
||||
rubyzip (2.3.0)
|
||||
safe_yaml (1.0.5)
|
||||
sanitize (5.2.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
@ -387,31 +381,32 @@ GEM
|
||||
seed-fu (2.3.9)
|
||||
activerecord (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
shoulda-matchers (4.3.0)
|
||||
shoulda-matchers (4.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.0.7)
|
||||
sidekiq (6.1.2)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
rack-protection (>= 2.0.0)
|
||||
redis (>= 4.1.0)
|
||||
simplecov (0.18.5)
|
||||
redis (>= 4.2.0)
|
||||
simplecov (0.20.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov-html (0.12.2)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.2)
|
||||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.1)
|
||||
sprockets-rails (3.2.2)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkey (2.0.0)
|
||||
stackprof (0.2.15)
|
||||
test-prof (0.11.3)
|
||||
stackprof (0.2.16)
|
||||
test-prof (0.12.2)
|
||||
thor (1.0.1)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
tzinfo (1.2.7)
|
||||
tzinfo (1.2.8)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
@ -419,32 +414,33 @@ GEM
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
unicorn (5.5.5)
|
||||
unicorn (5.7.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.13.0)
|
||||
webmock (3.8.3)
|
||||
webmock (3.10.0)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpush (1.0.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
xorcist (1.1.2)
|
||||
yaml-lint (0.0.10)
|
||||
zeitwerk (2.3.0)
|
||||
zeitwerk (2.4.1)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
actionmailer (= 6.0.3.1)
|
||||
actionpack (= 6.0.3.1)
|
||||
actionview (= 6.0.3.1)
|
||||
actionmailer (= 6.0.3.3)
|
||||
actionpack (= 6.0.3.3)
|
||||
actionview (= 6.0.3.3)
|
||||
actionview_precompiler
|
||||
active_model_serializers (~> 0.8.3)
|
||||
activemodel (= 6.0.3.1)
|
||||
activerecord (= 6.0.3.1)
|
||||
activesupport (= 6.0.3.1)
|
||||
activemodel (= 6.0.3.3)
|
||||
activerecord (= 6.0.3.3)
|
||||
activesupport (= 6.0.3.3)
|
||||
addressable
|
||||
annotate
|
||||
aws-sdk-s3
|
||||
@ -462,22 +458,22 @@ DEPENDENCIES
|
||||
cppjieba_rb
|
||||
css_parser
|
||||
diffy
|
||||
discourse-ember-rails (= 0.18.6)
|
||||
discourse-ember-source (~> 3.12.2)
|
||||
discourse-fonts
|
||||
discourse_image_optim
|
||||
email_reply_trimmer
|
||||
ember-handlebars-template (= 0.8.0)
|
||||
ember-rails (= 0.18.5)
|
||||
excon
|
||||
execjs
|
||||
fabrication
|
||||
fakeweb
|
||||
fast_blank
|
||||
fast_xor
|
||||
fast_xs
|
||||
fastimage
|
||||
flamegraph
|
||||
gc_tracer
|
||||
highline (~> 1.7.0)
|
||||
highline
|
||||
htmlentities
|
||||
http_accept_language
|
||||
json
|
||||
@ -508,7 +504,6 @@ DEPENDENCIES
|
||||
omniauth-facebook
|
||||
omniauth-github
|
||||
omniauth-google-oauth2
|
||||
omniauth-instagram
|
||||
omniauth-oauth2
|
||||
omniauth-twitter
|
||||
onebox
|
||||
@ -518,15 +513,14 @@ DEPENDENCIES
|
||||
pry-rails
|
||||
puma
|
||||
r2
|
||||
rack (= 2.2.2)
|
||||
rack
|
||||
rack-mini-profiler
|
||||
rack-protection
|
||||
rails_failover
|
||||
rails_multisite
|
||||
railties (= 6.0.3.1)
|
||||
railties (= 6.0.3.3)
|
||||
rake
|
||||
rb-fsevent
|
||||
rb-inotify (~> 0.9)
|
||||
rbtrace
|
||||
rchardet
|
||||
redis
|
||||
@ -560,6 +554,7 @@ DEPENDENCIES
|
||||
unicorn
|
||||
webmock
|
||||
webpush
|
||||
xorcist
|
||||
yaml-lint
|
||||
|
||||
BUNDLED WITH
|
||||
|
||||
@ -17,8 +17,9 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
|
||||
|
||||
<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://discuss.howtogeek.com"><img src="https://user-images.githubusercontent.com/1681963/52239247-04ad8280-289c-11e9-9706-fd66bc0749dc.png" width="720px"></a>
|
||||
<a href="https://talk.turtlerockstudios.com/"><img src="https://user-images.githubusercontent.com/1681963/52239249-04ad8280-289c-11e9-9155-f0ccc5decc50.png" width="720px"></a>
|
||||
<a href="https://discuss.atom.io/"><img src="https://user-images.githubusercontent.com/1681963/89088039-6735f080-d364-11ea-93a6-5629ea8738fe.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">
|
||||
|
||||
@ -61,6 +62,7 @@ Discourse supports the **latest, stable releases** of all major browsers and pla
|
||||
- [Ember.js](https://github.com/emberjs/ember.js) — Our front end is an Ember.js app that communicates with the Rails API.
|
||||
- [PostgreSQL](https://www.postgresql.org/) — Our main data store is in Postgres.
|
||||
- [Redis](https://redis.io/) — We use Redis as a cache and for transient data.
|
||||
- [BrowserStack](https://www.browserstack.com/) — We use BrowserStack to test on real devices and browsers.
|
||||
|
||||
Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https://github.com/discourse/discourse/blob/master/Gemfile).
|
||||
|
||||
@ -90,7 +92,6 @@ We take security very seriously at Discourse; all our code is 100% open source a
|
||||
|
||||
The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](https://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors).
|
||||
|
||||
|
||||
## Copyright / License
|
||||
|
||||
Copyright 2014 - 2020 Civilized Discourse Construction Kit, Inc.
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 882 B After Width: | Height: | Size: 810 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 937 B |
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 898 B |
|
Before Width: | Height: | Size: 895 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 613 B After Width: | Height: | Size: 525 B |
|
Before Width: | Height: | Size: 845 B After Width: | Height: | Size: 759 B |
@ -1,23 +1,18 @@
|
||||
// discourse-skip-module
|
||||
(function() {
|
||||
setTimeout(function() {
|
||||
(function () {
|
||||
setTimeout(function () {
|
||||
const $activateButton = $("#activate-account-button");
|
||||
$activateButton.on("click", function() {
|
||||
$activateButton.on("click", function () {
|
||||
$activateButton.prop("disabled", true);
|
||||
const hpPath = document.getElementById("data-activate-account").dataset
|
||||
.path;
|
||||
$.ajax(hpPath)
|
||||
.then(function(hp) {
|
||||
.then(function (hp) {
|
||||
$("#password_confirmation").val(hp.value);
|
||||
$("#challenge").val(
|
||||
hp.challenge
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("")
|
||||
);
|
||||
$("#challenge").val(hp.challenge.split("").reverse().join(""));
|
||||
$("#activate-account-form").submit();
|
||||
})
|
||||
.fail(function() {
|
||||
.fail(function () {
|
||||
$activateButton.prop("disabled", false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/api/";
|
||||
},
|
||||
|
||||
apiNameFor() {
|
||||
return "key";
|
||||
}
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default function buildPluginAdapter(pluginName) {
|
||||
return RestAdapter.extend({
|
||||
pathFor(store, type, findArgs) {
|
||||
return (
|
||||
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/customize/";
|
||||
}
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
pathFor() {
|
||||
return "/admin/customize/email_style";
|
||||
}
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
pathFor() {
|
||||
return "/admin/customize/embedding";
|
||||
}
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/logs/";
|
||||
}
|
||||
});
|
||||
@ -1,5 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
jsonMode: true
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/";
|
||||
},
|
||||
|
||||
afterFindAll(results) {
|
||||
let map = {};
|
||||
results.forEach(theme => {
|
||||
map[theme.id] = theme;
|
||||
});
|
||||
results.forEach(theme => {
|
||||
let mapped = theme.get("child_themes") || [];
|
||||
mapped = mapped.map(t => map[t.id]);
|
||||
theme.set("childThemes", mapped);
|
||||
|
||||
let mappedParents = theme.get("parent_themes") || [];
|
||||
mappedParents = mappedParents.map(t => map[t.id]);
|
||||
theme.set("parentThemes", mappedParents);
|
||||
});
|
||||
return results;
|
||||
},
|
||||
|
||||
jsonMode: true
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/api/";
|
||||
}
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/api/";
|
||||
}
|
||||
});
|
||||
13
app/assets/javascripts/admin/addon/adapters/api-key.js
Normal file
@ -0,0 +1,13 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
jsonMode: true,
|
||||
|
||||
basePath() {
|
||||
return "/admin/api/";
|
||||
},
|
||||
|
||||
apiNameFor() {
|
||||
return "key";
|
||||
},
|
||||
});
|
||||
11
app/assets/javascripts/admin/addon/adapters/build-plugin.js
Normal file
@ -0,0 +1,11 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default function buildPluginAdapter(pluginName) {
|
||||
return RestAdapter.extend({
|
||||
pathFor(store, type, findArgs) {
|
||||
return (
|
||||
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/customize/";
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
pathFor() {
|
||||
return "/admin/customize/email_style";
|
||||
},
|
||||
});
|
||||
7
app/assets/javascripts/admin/addon/adapters/embedding.js
Normal file
@ -0,0 +1,7 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
pathFor() {
|
||||
return "/admin/customize/embedding";
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/logs/";
|
||||
},
|
||||
});
|
||||
5
app/assets/javascripts/admin/addon/adapters/tag-group.js
Normal file
@ -0,0 +1,5 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
jsonMode: true,
|
||||
});
|
||||
26
app/assets/javascripts/admin/addon/adapters/theme.js
Normal file
@ -0,0 +1,26 @@
|
||||
import RestAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/";
|
||||
},
|
||||
|
||||
afterFindAll(results) {
|
||||
let map = {};
|
||||
results.forEach((theme) => {
|
||||
map[theme.id] = theme;
|
||||
});
|
||||
results.forEach((theme) => {
|
||||
let mapped = theme.get("child_themes") || [];
|
||||
mapped = mapped.map((t) => map[t.id]);
|
||||
theme.set("childThemes", mapped);
|
||||
|
||||
let mappedParents = theme.get("parent_themes") || [];
|
||||
mappedParents = mappedParents.map((t) => map[t.id]);
|
||||
theme.set("parentThemes", mappedParents);
|
||||
});
|
||||
return results;
|
||||
},
|
||||
|
||||
jsonMode: true,
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/api/";
|
||||
},
|
||||
});
|
||||
7
app/assets/javascripts/admin/addon/adapters/web-hook.js
Normal file
@ -0,0 +1,7 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/api/";
|
||||
},
|
||||
});
|
||||
134
app/assets/javascripts/admin/addon/components/ace-editor.js
Normal file
@ -0,0 +1,134 @@
|
||||
import Component from "@ember/component";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { on } from "@ember/object/evented";
|
||||
|
||||
export default Component.extend({
|
||||
mode: "css",
|
||||
classNames: ["ace-wrapper"],
|
||||
_editor: null,
|
||||
_skipContentChangeEvent: null,
|
||||
disabled: false,
|
||||
|
||||
@observes("editorId")
|
||||
editorIdChanged() {
|
||||
if (this.autofocus) {
|
||||
this.send("focus");
|
||||
}
|
||||
},
|
||||
|
||||
@observes("content")
|
||||
contentChanged() {
|
||||
const content = this.content || "";
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
this._editor.getSession().setValue(content);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("mode")
|
||||
modeChanged() {
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
this._editor.getSession().setMode("ace/mode/" + this.mode);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("placeholder")
|
||||
placeholderChanged() {
|
||||
if (this._editor) {
|
||||
this._editor.setOptions({
|
||||
placeholder: this.placeholder,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@observes("disabled")
|
||||
disabledStateChanged() {
|
||||
this.changeDisabledState();
|
||||
},
|
||||
|
||||
changeDisabledState() {
|
||||
const editor = this._editor;
|
||||
if (editor) {
|
||||
const disabled = this.disabled;
|
||||
editor.setOptions({
|
||||
readOnly: disabled,
|
||||
highlightActiveLine: !disabled,
|
||||
highlightGutterLine: !disabled,
|
||||
});
|
||||
editor.container.parentNode.setAttribute("data-disabled", disabled);
|
||||
}
|
||||
},
|
||||
|
||||
_destroyEditor: on("willDestroyElement", function () {
|
||||
if (this._editor) {
|
||||
this._editor.destroy();
|
||||
this._editor = null;
|
||||
}
|
||||
if (this.appEvents) {
|
||||
// xxx: don't run during qunit tests
|
||||
this.appEvents.off("ace:resize", this, "resize");
|
||||
}
|
||||
|
||||
$(window).off("ace:resize");
|
||||
}),
|
||||
|
||||
resize() {
|
||||
if (this._editor) {
|
||||
this._editor.resize();
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
loadScript("/javascripts/ace/ace.js").then(() => {
|
||||
window.ace.require(["ace/ace"], (loadedAce) => {
|
||||
loadedAce.config.set("loadWorkerFromBlob", false);
|
||||
loadedAce.config.set("workerPath", getURL("/javascripts/ace")); // Do not use CDN for workers
|
||||
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
const editor = loadedAce.edit(this.element.querySelector(".ace"));
|
||||
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.setOptions({ fontSize: "14px", placeholder: this.placeholder });
|
||||
editor.getSession().setMode("ace/mode/" + this.mode);
|
||||
editor.on("change", () => {
|
||||
this._skipContentChangeEvent = true;
|
||||
this.set("content", editor.getSession().getValue());
|
||||
this._skipContentChangeEvent = false;
|
||||
});
|
||||
editor.$blockScrolling = Infinity;
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
|
||||
this.element.setAttribute("data-editor", editor);
|
||||
this._editor = editor;
|
||||
this.changeDisabledState();
|
||||
|
||||
$(window)
|
||||
.off("ace:resize")
|
||||
.on("ace:resize", () => this.appEvents.trigger("ace:resize"));
|
||||
|
||||
if (this.appEvents) {
|
||||
// xxx: don't run during qunit tests
|
||||
this.appEvents.on("ace:resize", this, "resize");
|
||||
}
|
||||
|
||||
if (this.autofocus) {
|
||||
this.send("focus");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
focus() {
|
||||
if (this._editor) {
|
||||
this._editor.focus();
|
||||
this._editor.navigateFileEnd();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,76 @@
|
||||
import I18n from "I18n";
|
||||
import { scheduleOnce } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import discourseDebounce from "discourse/lib/debounce";
|
||||
import { observes, on } from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["admin-backups-logs"],
|
||||
showLoadingSpinner: false,
|
||||
hasFormattedLogs: false,
|
||||
noLogsMessage: I18n.t("admin.backups.logs.none"),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this._reset();
|
||||
},
|
||||
|
||||
_reset() {
|
||||
this.setProperties({ formattedLogs: "", index: 0 });
|
||||
},
|
||||
|
||||
_scrollDown() {
|
||||
const div = this.element;
|
||||
div.scrollTop = div.scrollHeight;
|
||||
},
|
||||
|
||||
@on("init")
|
||||
@observes("logs.[]")
|
||||
_resetFormattedLogs() {
|
||||
if (this.logs.length === 0) {
|
||||
this._reset(); // reset the cached logs whenever the model is reset
|
||||
this.renderLogs();
|
||||
}
|
||||
},
|
||||
|
||||
@on("init")
|
||||
@observes("logs.[]")
|
||||
_updateFormattedLogs: discourseDebounce(function () {
|
||||
const logs = this.logs;
|
||||
if (logs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do the log formatting only once for HELLish performance
|
||||
let formattedLogs = this.formattedLogs;
|
||||
for (let i = this.index, length = logs.length; i < length; i++) {
|
||||
const date = logs[i].get("timestamp"),
|
||||
message = logs[i].get("message");
|
||||
formattedLogs += "[" + date + "] " + message + "\n";
|
||||
}
|
||||
// update the formatted logs & cache index
|
||||
this.setProperties({
|
||||
formattedLogs: formattedLogs,
|
||||
index: logs.length,
|
||||
});
|
||||
// force rerender
|
||||
this.renderLogs();
|
||||
|
||||
scheduleOnce("afterRender", this, this._scrollDown);
|
||||
}, 150),
|
||||
|
||||
renderLogs() {
|
||||
const formattedLogs = this.formattedLogs;
|
||||
if (formattedLogs && formattedLogs.length > 0) {
|
||||
this.set("hasFormattedLogs", true);
|
||||
} else {
|
||||
this.set("hasFormattedLogs", false);
|
||||
}
|
||||
// add a loading indicator
|
||||
if (this.get("status.isOperationRunning")) {
|
||||
this.set("showLoadingSpinner", true);
|
||||
} else {
|
||||
this.set("showLoadingSpinner", false);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
|
||||
buffer: "",
|
||||
editing: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set("editing", false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
edit() {
|
||||
this.set("buffer", this.value);
|
||||
this.toggleProperty("editing");
|
||||
},
|
||||
|
||||
save() {
|
||||
// Action has to toggle 'editing' property.
|
||||
this.action(this.buffer);
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
classNames: ["row"],
|
||||
});
|
||||
57
app/assets/javascripts/admin/addon/components/admin-graph.js
Normal file
@ -0,0 +1,57 @@
|
||||
import Component from "@ember/component";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "canvas",
|
||||
type: "line",
|
||||
|
||||
refreshChart() {
|
||||
const ctx = this.element.getContext("2d");
|
||||
const model = this.model;
|
||||
const rawData = this.get("model.data");
|
||||
|
||||
var data = {
|
||||
labels: rawData.map((r) => r.x),
|
||||
datasets: [
|
||||
{
|
||||
data: rawData.map((r) => r.y),
|
||||
label: model.get("title"),
|
||||
backgroundColor: `rgba(200,220,240,${this.type === "bar" ? 1 : 0.3})`,
|
||||
borderColor: "#08C",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const config = {
|
||||
type: this.type,
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (context) =>
|
||||
moment(context[0].xLabel, "YYYY-MM-DD").format("LL"),
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
display: true,
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this._chart = new window.Chart(ctx, config);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
loadScript("/javascripts/Chart.min.js").then(() =>
|
||||
this.refreshChart.apply(this)
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
});
|
||||
@ -0,0 +1,239 @@
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import { debounce, schedule } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["admin-report-chart"],
|
||||
limit: 8,
|
||||
total: 0,
|
||||
options: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.resizeHandler = () =>
|
||||
debounce(this, this._scheduleChartRendering, 500);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(window).on("resize.chart", this.resizeHandler);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(window).off("resize.chart", this.resizeHandler);
|
||||
|
||||
this._resetChart();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
debounce(this, this._scheduleChartRendering, 100);
|
||||
},
|
||||
|
||||
_scheduleChartRendering() {
|
||||
schedule("afterRender", () => {
|
||||
this._renderChart(
|
||||
this.model,
|
||||
this.element && this.element.querySelector(".chart-canvas")
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_renderChart(model, chartCanvas) {
|
||||
if (!chartCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = chartCanvas.getContext("2d");
|
||||
const chartData = this._applyChartGrouping(
|
||||
model,
|
||||
makeArray(model.get("chartData") || model.get("data"), "weekly"),
|
||||
this.options
|
||||
);
|
||||
const prevChartData = makeArray(
|
||||
model.get("prevChartData") || model.get("prev_data")
|
||||
);
|
||||
|
||||
const labels = chartData.map((d) => d.x);
|
||||
|
||||
const data = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data: chartData.map((d) => Math.round(parseFloat(d.y))),
|
||||
backgroundColor: prevChartData.length
|
||||
? "transparent"
|
||||
: model.secondary_color,
|
||||
borderColor: model.primary_color,
|
||||
pointRadius: 3,
|
||||
borderWidth: 1,
|
||||
pointBackgroundColor: model.primary_color,
|
||||
pointBorderColor: model.primary_color,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (prevChartData.length) {
|
||||
data.datasets.push({
|
||||
data: prevChartData.map((d) => Math.round(parseFloat(d.y))),
|
||||
borderColor: model.primary_color,
|
||||
borderDash: [5, 5],
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1,
|
||||
pointRadius: 0,
|
||||
});
|
||||
}
|
||||
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
this._resetChart();
|
||||
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._chart = new window.Chart(
|
||||
context,
|
||||
this._buildChartConfig(data, this.options)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data, options) {
|
||||
return {
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (tooltipItem) =>
|
||||
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"),
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
responsiveAnimationDuration: 0,
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
display: true,
|
||||
ticks: {
|
||||
userCallback: (label) => {
|
||||
if (Math.floor(label) === label) {
|
||||
return label;
|
||||
}
|
||||
},
|
||||
callback: (label) => number(label),
|
||||
sampleSize: 5,
|
||||
maxRotation: 25,
|
||||
minRotation: 25,
|
||||
},
|
||||
},
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
display: true,
|
||||
gridLines: { display: false },
|
||||
type: "time",
|
||||
time: {
|
||||
unit: this._unitForGrouping(options),
|
||||
},
|
||||
ticks: {
|
||||
sampleSize: 5,
|
||||
maxRotation: 50,
|
||||
minRotation: 50,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
_resetChart() {
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
},
|
||||
|
||||
_applyChartGrouping(model, data, options) {
|
||||
if (!options.chartGrouping || options.chartGrouping === "daily") {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (
|
||||
options.chartGrouping === "weekly" ||
|
||||
options.chartGrouping === "monthly"
|
||||
) {
|
||||
const isoKind = options.chartGrouping === "weekly" ? "isoWeek" : "month";
|
||||
const kind = options.chartGrouping === "weekly" ? "week" : "month";
|
||||
const startMoment = moment(model.start_date, "YYYY-MM-DD");
|
||||
|
||||
let currentIndex = 0;
|
||||
let currentStart = startMoment.clone().startOf(isoKind);
|
||||
let currentEnd = startMoment.clone().endOf(isoKind);
|
||||
const transformedData = [
|
||||
{
|
||||
x: currentStart.format("YYYY-MM-DD"),
|
||||
y: 0,
|
||||
},
|
||||
];
|
||||
|
||||
data.forEach((d) => {
|
||||
let date = moment(d.x, "YYYY-MM-DD");
|
||||
|
||||
if (!date.isBetween(currentStart, currentEnd)) {
|
||||
currentIndex += 1;
|
||||
currentStart = currentStart.add(1, kind).startOf(isoKind);
|
||||
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
|
||||
}
|
||||
|
||||
if (transformedData[currentIndex]) {
|
||||
transformedData[currentIndex].y += d.y;
|
||||
} else {
|
||||
transformedData[currentIndex] = {
|
||||
x: d.x,
|
||||
y: d.y,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
// ensure we return something if grouping is unknown
|
||||
return data;
|
||||
},
|
||||
|
||||
_unitForGrouping(options) {
|
||||
switch (options.chartGrouping) {
|
||||
case "monthly":
|
||||
return "month";
|
||||
case "weekly":
|
||||
return "week";
|
||||
default:
|
||||
return "day";
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
classNames: ["admin-report-counters"],
|
||||
|
||||
attributeBindings: ["model.description:title"],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { match } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
allTime: true,
|
||||
tagName: "tr",
|
||||
reverseColors: match(
|
||||
"report.type",
|
||||
/^(time_to_first_response|topics_with_no_response)$/
|
||||
),
|
||||
classNameBindings: ["reverseColors"],
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
classNames: ["admin-report-inline-table"],
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "tr",
|
||||
});
|
||||
@ -0,0 +1,159 @@
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import { debounce, schedule } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.resizeHandler = () =>
|
||||
debounce(this, this._scheduleChartRendering, 500);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(window).on("resize.chart", this.resizeHandler);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(window).off("resize.chart", this.resizeHandler);
|
||||
|
||||
this._resetChart();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
debounce(this, this._scheduleChartRendering, 100);
|
||||
},
|
||||
|
||||
_scheduleChartRendering() {
|
||||
schedule("afterRender", () => {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderChart(
|
||||
this.model,
|
||||
this.element.querySelector(".chart-canvas")
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_renderChart(model, chartCanvas) {
|
||||
if (!chartCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = chartCanvas.getContext("2d");
|
||||
|
||||
const chartData = makeArray(model.get("chartData") || model.get("data"));
|
||||
|
||||
const data = {
|
||||
labels: chartData[0].data.mapBy("x"),
|
||||
datasets: chartData.map((cd) => {
|
||||
return {
|
||||
label: cd.label,
|
||||
stack: "pageviews-stack",
|
||||
data: cd.data.map((d) => Math.round(parseFloat(d.y))),
|
||||
backgroundColor: cd.color,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
this._resetChart();
|
||||
|
||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data) {
|
||||
return {
|
||||
type: "bar",
|
||||
data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
responsiveAnimationDuration: 0,
|
||||
hover: { mode: "index" },
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
tooltips: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
beforeFooter: (tooltipItem) => {
|
||||
let total = 0;
|
||||
tooltipItem.forEach(
|
||||
(item) => (total += parseInt(item.yLabel || 0, 10))
|
||||
);
|
||||
return `= ${total}`;
|
||||
},
|
||||
title: (tooltipItem) =>
|
||||
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL"),
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
stacked: true,
|
||||
display: true,
|
||||
ticks: {
|
||||
userCallback: (label) => {
|
||||
if (Math.floor(label) === label) {
|
||||
return label;
|
||||
}
|
||||
},
|
||||
callback: (label) => number(label),
|
||||
sampleSize: 5,
|
||||
maxRotation: 25,
|
||||
minRotation: 25,
|
||||
},
|
||||
},
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
display: true,
|
||||
gridLines: { display: false },
|
||||
type: "time",
|
||||
offset: true,
|
||||
time: {
|
||||
parser: "YYYY-MM-DD",
|
||||
minUnit: "day",
|
||||
},
|
||||
ticks: {
|
||||
sampleSize: 5,
|
||||
maxRotation: 50,
|
||||
minRotation: 50,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
_resetChart() {
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,43 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["admin-report-storage-stats"],
|
||||
|
||||
backupLocation: setting("backup_location"),
|
||||
backupStats: alias("model.data.backups"),
|
||||
uploadStats: alias("model.data.uploads"),
|
||||
|
||||
@discourseComputed("backupStats")
|
||||
showBackupStats(stats) {
|
||||
return stats && this.currentUser.admin;
|
||||
},
|
||||
|
||||
@discourseComputed("backupLocation")
|
||||
backupLocationName(backupLocation) {
|
||||
return I18n.t(`admin.backups.location.${backupLocation}`);
|
||||
},
|
||||
|
||||
@discourseComputed("backupStats.used_bytes")
|
||||
usedBackupSpace(bytes) {
|
||||
return I18n.toHumanSize(bytes);
|
||||
},
|
||||
|
||||
@discourseComputed("backupStats.free_bytes")
|
||||
freeBackupSpace(bytes) {
|
||||
return I18n.toHumanSize(bytes);
|
||||
},
|
||||
|
||||
@discourseComputed("uploadStats.used_bytes")
|
||||
usedUploadSpace(bytes) {
|
||||
return I18n.toHumanSize(bytes);
|
||||
},
|
||||
|
||||
@discourseComputed("uploadStats.free_bytes")
|
||||
freeUploadSpace(bytes) {
|
||||
return I18n.toHumanSize(bytes);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "td",
|
||||
classNames: ["admin-report-table-cell"],
|
||||
classNameBindings: ["type", "property"],
|
||||
options: null,
|
||||
|
||||
@discourseComputed("label", "data", "options")
|
||||
computedLabel(label, data, options) {
|
||||
return label.compute(data, options || {});
|
||||
},
|
||||
|
||||
type: alias("label.type"),
|
||||
property: alias("label.mainProperty"),
|
||||
formatedValue: alias("computedLabel.formatedValue"),
|
||||
value: alias("computedLabel.value"),
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "th",
|
||||
classNames: ["admin-report-table-header"],
|
||||
classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
|
||||
attributeBindings: ["label.title:title"],
|
||||
|
||||
@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
|
||||
isCurrentSort(currentSortField, labelSortField) {
|
||||
return currentSortField === labelSortField;
|
||||
},
|
||||
|
||||
@discourseComputed("currentSortDirection")
|
||||
sortIcon(currentSortDirection) {
|
||||
return currentSortDirection === 1 ? "caret-up" : "caret-down";
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "tr",
|
||||
classNames: ["admin-report-table-row"],
|
||||
options: null,
|
||||
});
|
||||
@ -0,0 +1,174 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
|
||||
const PAGES_LIMIT = 8;
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: ["sortable", "twoColumns"],
|
||||
classNames: ["admin-report-table"],
|
||||
sortable: false,
|
||||
sortDirection: 1,
|
||||
perPage: alias("options.perPage"),
|
||||
page: 0,
|
||||
|
||||
@discourseComputed("model.computedLabels.length")
|
||||
twoColumns(labelsLength) {
|
||||
return labelsLength === 2;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"totalsForSample",
|
||||
"options.total",
|
||||
"model.dates_filtering"
|
||||
)
|
||||
showTotalForSample(totalsForSample, total, datesFiltering) {
|
||||
// check if we have at least one cell which contains a value
|
||||
const sum = totalsForSample
|
||||
.map((t) => t.value)
|
||||
.compact()
|
||||
.reduce((s, v) => s + v, 0);
|
||||
|
||||
return sum >= 1 && total && datesFiltering;
|
||||
},
|
||||
|
||||
@discourseComputed("model.total", "options.total", "twoColumns")
|
||||
showTotal(reportTotal, total, twoColumns) {
|
||||
return reportTotal && total && twoColumns;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"model.{average,data}",
|
||||
"totalsForSample.1.value",
|
||||
"twoColumns"
|
||||
)
|
||||
showAverage(model, sampleTotalValue, hasTwoColumns) {
|
||||
return (
|
||||
model.average &&
|
||||
model.data.length > 0 &&
|
||||
sampleTotalValue &&
|
||||
hasTwoColumns
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("totalsForSample.1.value", "model.data.length")
|
||||
averageForSample(totals, count) {
|
||||
return (totals / count).toFixed(0);
|
||||
},
|
||||
|
||||
@discourseComputed("model.data.length")
|
||||
showSortingUI(dataLength) {
|
||||
return dataLength >= 5;
|
||||
},
|
||||
|
||||
@discourseComputed("totalsForSampleRow", "model.computedLabels")
|
||||
totalsForSample(row, labels) {
|
||||
return labels.map((label) => {
|
||||
const computedLabel = label.compute(row);
|
||||
computedLabel.type = label.type;
|
||||
computedLabel.property = label.mainProperty;
|
||||
return computedLabel;
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("model.data", "model.computedLabels")
|
||||
totalsForSampleRow(rows, labels) {
|
||||
if (!rows || !rows.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let totalsRow = {};
|
||||
|
||||
labels.forEach((label) => {
|
||||
const reducer = (sum, row) => {
|
||||
const computedLabel = label.compute(row);
|
||||
const value = computedLabel.value;
|
||||
|
||||
if (!["seconds", "number", "percent"].includes(label.type)) {
|
||||
return;
|
||||
} else {
|
||||
return sum + Math.round(value || 0);
|
||||
}
|
||||
};
|
||||
|
||||
const total = rows.reduce(reducer, 0);
|
||||
totalsRow[label.mainProperty] =
|
||||
label.type === "percent" ? Math.round(total / rows.length) : total;
|
||||
});
|
||||
|
||||
return totalsRow;
|
||||
},
|
||||
|
||||
@discourseComputed("sortLabel", "sortDirection", "model.data.[]")
|
||||
sortedData(sortLabel, sortDirection, data) {
|
||||
data = makeArray(data);
|
||||
|
||||
if (sortLabel) {
|
||||
const compare = (label, direction) => {
|
||||
return (a, b) => {
|
||||
const aValue = label.compute(a, { useSortProperty: true }).value;
|
||||
const bValue = label.compute(b, { useSortProperty: true }).value;
|
||||
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||||
return result * direction;
|
||||
};
|
||||
};
|
||||
|
||||
return data.sort(compare(sortLabel, sortDirection));
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@discourseComputed("sortedData.[]", "perPage", "page")
|
||||
paginatedData(data, perPage, page) {
|
||||
if (perPage < data.length) {
|
||||
const start = perPage * page;
|
||||
return data.slice(start, start + perPage);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
@discourseComputed("model.data", "perPage", "page")
|
||||
pages(data, perPage, page) {
|
||||
if (!data || data.length <= perPage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pagesIndexes = [];
|
||||
for (let i = 0; i < Math.ceil(data.length / perPage); i++) {
|
||||
pagesIndexes.push(i);
|
||||
}
|
||||
|
||||
let pages = pagesIndexes.map((v) => {
|
||||
return {
|
||||
page: v + 1,
|
||||
index: v,
|
||||
class: v === page ? "is-current" : null,
|
||||
};
|
||||
});
|
||||
|
||||
if (pages.length > PAGES_LIMIT) {
|
||||
const before = Math.max(0, page - PAGES_LIMIT / 2);
|
||||
const after = Math.max(PAGES_LIMIT, page + PAGES_LIMIT / 2);
|
||||
pages = pages.slice(before, after);
|
||||
}
|
||||
|
||||
return pages;
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePage(page) {
|
||||
this.set("page", page);
|
||||
},
|
||||
|
||||
sortByLabel(label) {
|
||||
if (this.sortLabel === label) {
|
||||
this.set("sortDirection", this.sortDirection === 1 ? -1 : 1);
|
||||
} else {
|
||||
this.set("sortLabel", label);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "tr",
|
||||
});
|
||||
453
app/assets/javascripts/admin/addon/components/admin-report.js
Normal file
@ -0,0 +1,453 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import { alias, or, and, equal, notEmpty } from "@ember/object/computed";
|
||||
import EmberObject, { computed, action } from "@ember/object";
|
||||
import { next } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import ReportLoader from "discourse/lib/reports-loader";
|
||||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { outputExportResult } from "discourse/lib/export-result";
|
||||
import Report, { SCHEMA_VERSION } from "admin/models/report";
|
||||
import { isPresent } from "@ember/utils";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
const TABLE_OPTIONS = {
|
||||
perPage: 8,
|
||||
total: true,
|
||||
limit: 20,
|
||||
formatNumbers: true,
|
||||
};
|
||||
|
||||
const CHART_OPTIONS = {};
|
||||
|
||||
function collapseWeekly(data, average) {
|
||||
let aggregate = [];
|
||||
let bucket, i;
|
||||
let offset = data.length % 7;
|
||||
for (i = offset; i < data.length; i++) {
|
||||
if (bucket && i % 7 === offset) {
|
||||
if (average) {
|
||||
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
|
||||
}
|
||||
aggregate.push(bucket);
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
bucket = bucket || { x: data[i].x, y: 0 };
|
||||
bucket.y += data[i].y;
|
||||
}
|
||||
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: [
|
||||
"isHidden:hidden",
|
||||
"isHidden::is-visible",
|
||||
"isEnabled",
|
||||
"isLoading",
|
||||
"dasherizedDataSourceName",
|
||||
],
|
||||
classNames: ["admin-report"],
|
||||
isEnabled: true,
|
||||
disabledLabel: I18n.t("admin.dashboard.disabled"),
|
||||
isLoading: false,
|
||||
rateLimitationString: null,
|
||||
dataSourceName: null,
|
||||
report: null,
|
||||
model: null,
|
||||
reportOptions: null,
|
||||
forcedModes: null,
|
||||
showAllReportsLink: false,
|
||||
filters: null,
|
||||
showTrend: false,
|
||||
showHeader: true,
|
||||
showTitle: true,
|
||||
showFilteringUI: false,
|
||||
showDatesOptions: alias("model.dates_filtering"),
|
||||
showRefresh: or("showDatesOptions", "model.available_filters.length"),
|
||||
shouldDisplayTrend: and("showTrend", "model.prev_period"),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this._reports = [];
|
||||
},
|
||||
|
||||
isHidden: computed("siteSettings.dashboard_hidden_reports", function () {
|
||||
return (this.siteSettings.dashboard_hidden_reports || "")
|
||||
.split("|")
|
||||
.filter(Boolean)
|
||||
.includes(this.dataSourceName);
|
||||
}),
|
||||
|
||||
startDate: computed("filters.startDate", function () {
|
||||
if (this.filters && isPresent(this.filters.startDate)) {
|
||||
return moment(this.filters.startDate, "YYYY-MM-DD");
|
||||
} else {
|
||||
return moment();
|
||||
}
|
||||
}),
|
||||
|
||||
endDate: computed("filters.endDate", function () {
|
||||
if (this.filters && isPresent(this.filters.endDate)) {
|
||||
return moment(this.filters.endDate, "YYYY-MM-DD");
|
||||
} else {
|
||||
return moment();
|
||||
}
|
||||
}),
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.report) {
|
||||
this._renderReport(this.report, this.forcedModes, this.currentMode);
|
||||
} else if (this.dataSourceName) {
|
||||
this._fetchReport();
|
||||
}
|
||||
},
|
||||
|
||||
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
|
||||
showNotFoundError: equal("model.error", "not_found"),
|
||||
showTimeoutError: equal("model.error", "timeout"),
|
||||
showExceptionError: equal("model.error", "exception"),
|
||||
|
||||
hasData: notEmpty("model.data"),
|
||||
|
||||
@discourseComputed("dataSourceName", "model.type")
|
||||
dasherizedDataSourceName(dataSourceName, type) {
|
||||
return (dataSourceName || type || "undefined").replace(/_/g, "-");
|
||||
},
|
||||
|
||||
@discourseComputed("dataSourceName", "model.type")
|
||||
dataSource(dataSourceName, type) {
|
||||
dataSourceName = dataSourceName || type;
|
||||
return `/admin/reports/${dataSourceName}`;
|
||||
},
|
||||
|
||||
@discourseComputed("displayedModes.length")
|
||||
showModes(displayedModesLength) {
|
||||
return displayedModesLength > 1;
|
||||
},
|
||||
|
||||
@discourseComputed("currentMode")
|
||||
isChartMode(currentMode) {
|
||||
return currentMode === "chart";
|
||||
},
|
||||
|
||||
@action
|
||||
changeGrouping(grouping) {
|
||||
this.send("refreshReport", {
|
||||
chartGrouping: grouping,
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("currentMode", "model.modes", "forcedModes")
|
||||
displayedModes(currentMode, reportModes, forcedModes) {
|
||||
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
||||
|
||||
return makeArray(modes).map((mode) => {
|
||||
const base = `btn-default mode-btn ${mode}`;
|
||||
const cssClass = currentMode === mode ? `${base} is-current` : base;
|
||||
|
||||
return {
|
||||
mode,
|
||||
cssClass,
|
||||
icon: mode === "table" ? "table" : "signal",
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("currentMode")
|
||||
modeComponent(currentMode) {
|
||||
return `admin-report-${currentMode.replace(/_/g, "-")}`;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"dataSourceName",
|
||||
"startDate",
|
||||
"endDate",
|
||||
"filters.customFilters"
|
||||
)
|
||||
reportKey(dataSourceName, startDate, endDate, customFilters) {
|
||||
if (!dataSourceName || !startDate || !endDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
startDate = startDate.toISOString(true).split("T")[0];
|
||||
endDate = endDate.toISOString(true).split("T")[0];
|
||||
|
||||
let reportKey = "reports:";
|
||||
reportKey += [
|
||||
dataSourceName,
|
||||
isTesting() ? "start" : startDate.replace(/-/g, ""),
|
||||
isTesting() ? "end" : endDate.replace(/-/g, ""),
|
||||
"[:prev_period]",
|
||||
this.get("reportOptions.table.limit"),
|
||||
// Convert all filter values to strings to ensure unique serialization
|
||||
customFilters
|
||||
? JSON.stringify(customFilters, (k, v) => (k ? `${v}` : v))
|
||||
: null,
|
||||
SCHEMA_VERSION,
|
||||
]
|
||||
.filter((x) => x)
|
||||
.map((x) => x.toString())
|
||||
.join(":");
|
||||
|
||||
return reportKey;
|
||||
},
|
||||
|
||||
@discourseComputed("reportOptions.chartGrouping")
|
||||
chartGroupings(chartGrouping) {
|
||||
chartGrouping = chartGrouping || "daily";
|
||||
|
||||
return ["daily", "weekly", "monthly"].map((id) => {
|
||||
return {
|
||||
id,
|
||||
label: `admin.dashboard.reports.${id}`,
|
||||
class: `chart-grouping ${chartGrouping === id ? "active" : "inactive"}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
onChangeDateRange(range) {
|
||||
this.send("refreshReport", {
|
||||
startDate: range.from,
|
||||
endDate: range.to,
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
applyFilter(id, value) {
|
||||
let customFilters = this.get("filters.customFilters") || {};
|
||||
|
||||
if (typeof value === "undefined") {
|
||||
delete customFilters[id];
|
||||
} else {
|
||||
customFilters[id] = value;
|
||||
}
|
||||
|
||||
this.send("refreshReport", {
|
||||
filters: customFilters,
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
refreshReport(options = {}) {
|
||||
if (!this.attrs.onRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attrs.onRefresh({
|
||||
type: this.get("model.type"),
|
||||
chartGrouping: options.chartGrouping,
|
||||
startDate:
|
||||
typeof options.startDate === "undefined"
|
||||
? this.startDate
|
||||
: options.startDate,
|
||||
endDate:
|
||||
typeof options.endDate === "undefined" ? this.endDate : options.endDate,
|
||||
filters:
|
||||
typeof options.filters === "undefined"
|
||||
? this.get("filters.customFilters")
|
||||
: options.filters,
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
exportCsv() {
|
||||
const args = {
|
||||
name: this.get("model.type"),
|
||||
start_date: this.startDate.toISOString(true).split("T")[0],
|
||||
end_date: this.endDate.toISOString(true).split("T")[0],
|
||||
};
|
||||
|
||||
const customFilters = this.get("filters.customFilters");
|
||||
if (customFilters) {
|
||||
Object.assign(args, customFilters);
|
||||
}
|
||||
|
||||
exportEntity("report", args).then(outputExportResult);
|
||||
},
|
||||
|
||||
@action
|
||||
changeMode(mode) {
|
||||
this.set("currentMode", mode);
|
||||
|
||||
this.send("refreshReport", {
|
||||
chartGrouping: null,
|
||||
});
|
||||
},
|
||||
|
||||
_computeReport() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._reports || !this._reports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// on a slow network _fetchReport could be called multiple times between
|
||||
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
||||
// to avoid any inconsistencies we filter by period and make sure
|
||||
// the array contains only unique values
|
||||
let filteredReports = this._reports.uniqBy("report_key");
|
||||
let report;
|
||||
|
||||
const sort = (r) => {
|
||||
if (r.length > 1) {
|
||||
return r.findBy("type", this.dataSourceName);
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.startDate || !this.endDate) {
|
||||
report = sort(filteredReports)[0];
|
||||
} else {
|
||||
report = sort(
|
||||
filteredReports.filter((r) => r.report_key.includes(this.reportKey))
|
||||
)[0];
|
||||
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (report.error === "not_found") {
|
||||
this.set("showFilteringUI", false);
|
||||
}
|
||||
|
||||
this._renderReport(report, this.forcedModes, this.currentMode);
|
||||
},
|
||||
|
||||
_renderReport(report, forcedModes, currentMode) {
|
||||
const modes = forcedModes ? forcedModes.split(",") : report.modes;
|
||||
currentMode = currentMode || (modes ? modes[0] : null);
|
||||
|
||||
this.setProperties({
|
||||
model: report,
|
||||
currentMode,
|
||||
options: this._buildOptions(currentMode),
|
||||
});
|
||||
},
|
||||
|
||||
_fetchReport() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.setProperties({ isLoading: true, rateLimitationString: null });
|
||||
|
||||
next(() => {
|
||||
let payload = this._buildPayload(["prev_period"]);
|
||||
|
||||
const callback = (response) => {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("isLoading", false);
|
||||
|
||||
if (response === 429) {
|
||||
this.set(
|
||||
"rateLimitationString",
|
||||
I18n.t("admin.dashboard.too_many_requests")
|
||||
);
|
||||
} else if (response === 500) {
|
||||
this.set("model.error", "exception");
|
||||
} else if (response) {
|
||||
this._reports.push(this._loadReport(response));
|
||||
this._computeReport();
|
||||
}
|
||||
};
|
||||
|
||||
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
|
||||
});
|
||||
},
|
||||
|
||||
_buildPayload(facets) {
|
||||
let payload = { data: { cache: true, facets } };
|
||||
|
||||
if (this.startDate) {
|
||||
payload.data.start_date = moment(this.startDate)
|
||||
.toISOString(true)
|
||||
.split("T")[0];
|
||||
}
|
||||
|
||||
if (this.endDate) {
|
||||
payload.data.end_date = moment(this.endDate)
|
||||
.toISOString(true)
|
||||
.split("T")[0];
|
||||
}
|
||||
|
||||
if (this.get("reportOptions.table.limit")) {
|
||||
payload.data.limit = this.get("reportOptions.table.limit");
|
||||
}
|
||||
|
||||
if (this.get("filters.customFilters")) {
|
||||
payload.data.filters = this.get("filters.customFilters");
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
_buildOptions(mode) {
|
||||
if (mode === "table") {
|
||||
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
|
||||
return EmberObject.create(
|
||||
Object.assign(tableOptions, this.get("reportOptions.table") || {})
|
||||
);
|
||||
} else {
|
||||
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
||||
return EmberObject.create(
|
||||
Object.assign(chartOptions, this.get("reportOptions.chart") || {}, {
|
||||
chartGrouping: this.get("reportOptions.chartGrouping"),
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_loadReport(jsonReport) {
|
||||
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
||||
|
||||
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
|
||||
jsonReport.chartData = jsonReport.chartData.map((chartData) => {
|
||||
if (chartData.length > 40) {
|
||||
return {
|
||||
data: collapseWeekly(chartData.data),
|
||||
req: chartData.req,
|
||||
label: chartData.label,
|
||||
color: chartData.color,
|
||||
};
|
||||
} else {
|
||||
return chartData;
|
||||
}
|
||||
});
|
||||
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
|
||||
jsonReport.chartData = collapseWeekly(
|
||||
jsonReport.chartData,
|
||||
jsonReport.average
|
||||
);
|
||||
}
|
||||
|
||||
if (jsonReport.prev_data) {
|
||||
Report.fillMissingDates(jsonReport, {
|
||||
filledField: "prevChartData",
|
||||
dataField: "prev_data",
|
||||
starDate: jsonReport.prev_startDate,
|
||||
endDate: jsonReport.prev_endDate,
|
||||
});
|
||||
|
||||
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
|
||||
jsonReport.prevChartData = collapseWeekly(
|
||||
jsonReport.prevChartData,
|
||||
jsonReport.average
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Report.create(jsonReport);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,115 @@
|
||||
import I18n from "I18n";
|
||||
import { next } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { fmt } from "discourse/lib/computed";
|
||||
|
||||
export default Component.extend({
|
||||
@discourseComputed("theme.targets", "onlyOverridden", "showAdvanced")
|
||||
visibleTargets(targets, onlyOverridden, showAdvanced) {
|
||||
return targets.filter((target) => {
|
||||
if (target.advanced && !showAdvanced) {
|
||||
return false;
|
||||
}
|
||||
if (!onlyOverridden) {
|
||||
return true;
|
||||
}
|
||||
return target.edited;
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("currentTargetName", "onlyOverridden", "theme.fields")
|
||||
visibleFields(targetName, onlyOverridden, fields) {
|
||||
fields = fields[targetName];
|
||||
if (onlyOverridden) {
|
||||
fields = fields.filter((field) => field.edited);
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
|
||||
@discourseComputed("currentTargetName", "fieldName")
|
||||
activeSectionMode(targetName, fieldName) {
|
||||
if (["settings", "translations"].includes(targetName)) {
|
||||
return "yaml";
|
||||
}
|
||||
if (["extra_scss"].includes(targetName)) {
|
||||
return "scss";
|
||||
}
|
||||
if (["color_definitions"].includes(fieldName)) {
|
||||
return "scss";
|
||||
}
|
||||
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
||||
},
|
||||
|
||||
@discourseComputed("currentTargetName", "fieldName")
|
||||
placeholder(targetName, fieldName) {
|
||||
return fieldName && fieldName === "color_definitions"
|
||||
? I18n.t("admin.customize.theme.color_definitions.placeholder")
|
||||
: "";
|
||||
},
|
||||
|
||||
@discourseComputed("fieldName", "currentTargetName", "theme")
|
||||
activeSection: {
|
||||
get(fieldName, target, model) {
|
||||
return model.getField(target, fieldName);
|
||||
},
|
||||
set(value, fieldName, target, model) {
|
||||
model.setField(target, fieldName, value);
|
||||
return value;
|
||||
},
|
||||
},
|
||||
|
||||
editorId: fmt("fieldName", "currentTargetName", "%@|%@"),
|
||||
|
||||
@discourseComputed("maximized")
|
||||
maximizeIcon(maximized) {
|
||||
return maximized ? "discourse-compress" : "discourse-expand";
|
||||
},
|
||||
|
||||
@discourseComputed("currentTargetName", "theme.targets")
|
||||
showAddField(currentTargetName, targets) {
|
||||
return targets.find((t) => t.name === currentTargetName).customNames;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"currentTargetName",
|
||||
"fieldName",
|
||||
"theme.theme_fields.@each.error"
|
||||
)
|
||||
error(target, fieldName) {
|
||||
return this.theme.getError(target, fieldName);
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleShowAdvanced() {
|
||||
this.toggleProperty("showAdvanced");
|
||||
},
|
||||
|
||||
toggleAddField() {
|
||||
this.toggleProperty("addingField");
|
||||
},
|
||||
|
||||
cancelAddField() {
|
||||
this.set("addingField", false);
|
||||
},
|
||||
|
||||
addField(name) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
name = name.replace(/[^a-zA-Z0-9-_/]/g, "");
|
||||
this.theme.setField(this.currentTargetName, name, "");
|
||||
this.setProperties({ newFieldName: "", addingField: false });
|
||||
this.fieldAdded(this.currentTargetName, name);
|
||||
},
|
||||
|
||||
toggleMaximize: function () {
|
||||
this.toggleProperty("maximized");
|
||||
next(() => this.appEvents.trigger("ace:resize"));
|
||||
},
|
||||
|
||||
onlyOverriddenChanged(value) {
|
||||
this.onlyOverriddenChanged(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,107 @@
|
||||
import I18n from "I18n";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { empty } from "@ember/object/computed";
|
||||
import { scheduleOnce } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import UserField from "admin/models/user-field";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { propertyEqual, i18n } from "discourse/lib/computed";
|
||||
import discourseComputed, {
|
||||
observes,
|
||||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend(bufferedProperty("userField"), {
|
||||
editing: empty("userField.id"),
|
||||
classNameBindings: [":user-field"],
|
||||
|
||||
cantMoveUp: propertyEqual("userField", "firstField"),
|
||||
cantMoveDown: propertyEqual("userField", "lastField"),
|
||||
|
||||
userFieldsDescription: i18n("admin.user_fields.description"),
|
||||
|
||||
@discourseComputed("buffered.field_type")
|
||||
bufferedFieldType(fieldType) {
|
||||
return UserField.fieldTypeById(fieldType);
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
@observes("editing")
|
||||
_focusOnEdit() {
|
||||
if (this.editing) {
|
||||
scheduleOnce("afterRender", this, "_focusName");
|
||||
}
|
||||
},
|
||||
|
||||
_focusName() {
|
||||
$(".user-field-name").select();
|
||||
},
|
||||
|
||||
@discourseComputed("userField.field_type")
|
||||
fieldName(fieldType) {
|
||||
return UserField.fieldTypeById(fieldType).get("name");
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"userField.editable",
|
||||
"userField.required",
|
||||
"userField.show_on_profile",
|
||||
"userField.show_on_user_card"
|
||||
)
|
||||
flags(editable, required, showOnProfile, showOnUserCard) {
|
||||
const ret = [];
|
||||
if (editable) {
|
||||
ret.push(I18n.t("admin.user_fields.editable.enabled"));
|
||||
}
|
||||
if (required) {
|
||||
ret.push(I18n.t("admin.user_fields.required.enabled"));
|
||||
}
|
||||
if (showOnProfile) {
|
||||
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
|
||||
}
|
||||
if (showOnUserCard) {
|
||||
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
|
||||
}
|
||||
|
||||
return ret.join(", ");
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
const buffered = this.buffered;
|
||||
const attrs = buffered.getProperties(
|
||||
"name",
|
||||
"description",
|
||||
"field_type",
|
||||
"editable",
|
||||
"required",
|
||||
"show_on_profile",
|
||||
"show_on_user_card",
|
||||
"options"
|
||||
);
|
||||
|
||||
this.userField
|
||||
.save(attrs)
|
||||
.then(() => {
|
||||
this.set("editing", false);
|
||||
this.commitBuffer();
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
edit() {
|
||||
this.set("editing", true);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
const id = this.get("userField.id");
|
||||
if (isEmpty(id)) {
|
||||
this.destroyAction(this.userField);
|
||||
} else {
|
||||
this.rollbackBuffer();
|
||||
this.set("editing", false);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import I18n from "I18n";
|
||||
import Component from "@ember/component";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["watched-word"],
|
||||
watchedWord: null,
|
||||
xIcon: iconHTML("times").htmlSafe(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set("watchedWord", this.get("word.word"));
|
||||
},
|
||||
|
||||
click() {
|
||||
this.word
|
||||
.destroy()
|
||||
.then(() => {
|
||||
this.action(this.word);
|
||||
})
|
||||
.catch((e) => {
|
||||
bootbox.alert(
|
||||
I18n.t("generic_error_with_reason", {
|
||||
error: `http: ${e.status} - ${e.body}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["hook-event"],
|
||||
typeName: alias("type.name"),
|
||||
|
||||
@discourseComputed("typeName")
|
||||
name(typeName) {
|
||||
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
|
||||
},
|
||||
|
||||
@discourseComputed("typeName")
|
||||
details(typeName) {
|
||||
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
|
||||
},
|
||||
|
||||
@discourseComputed("model.[]", "typeName")
|
||||
eventTypeExists(eventTypes, typeName) {
|
||||
return eventTypes.any((event) => event.name === typeName);
|
||||
},
|
||||
|
||||
@discourseComputed("eventTypeExists")
|
||||
enabled: {
|
||||
get(eventTypeExists) {
|
||||
return eventTypeExists;
|
||||
},
|
||||
set(value, eventTypeExists) {
|
||||
const type = this.type;
|
||||
const model = this.model;
|
||||
// add an association when not exists
|
||||
if (value !== eventTypeExists) {
|
||||
if (value) {
|
||||
model.addObject(type);
|
||||
} else {
|
||||
model.removeObjects(
|
||||
model.filter((eventType) => eventType.name === type.name)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,113 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "li",
|
||||
expandDetails: null,
|
||||
expandDetailsRequestKey: "request",
|
||||
expandDetailsResponseKey: "response",
|
||||
|
||||
@discourseComputed("model.status")
|
||||
statusColorClasses(status) {
|
||||
if (!status) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (status >= 200 && status <= 299) {
|
||||
return "text-successful";
|
||||
} else {
|
||||
return "text-danger";
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("model.created_at")
|
||||
createdAt(createdAt) {
|
||||
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
|
||||
@discourseComputed("model.duration")
|
||||
completion(duration) {
|
||||
const seconds = Math.floor(duration / 10.0) / 100.0;
|
||||
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
|
||||
},
|
||||
|
||||
@discourseComputed("expandDetails")
|
||||
expandRequestIcon(expandDetails) {
|
||||
return expandDetails === this.expandDetailsRequestKey
|
||||
? "ellipsis-h"
|
||||
: "ellipsis-v";
|
||||
},
|
||||
|
||||
@discourseComputed("expandDetails")
|
||||
expandResponseIcon(expandDetails) {
|
||||
return expandDetails === this.expandDetailsResponseKey
|
||||
? "ellipsis-h"
|
||||
: "ellipsis-v";
|
||||
},
|
||||
|
||||
actions: {
|
||||
redeliver() {
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
toggleRequest() {
|
||||
const expandDetailsKey = this.expandDetailsRequestKey;
|
||||
|
||||
if (this.expandDetails !== expandDetailsKey) {
|
||||
let headers = Object.assign(
|
||||
{
|
||||
"Request URL": this.get("model.request_url"),
|
||||
"Request method": "POST",
|
||||
},
|
||||
ensureJSON(this.get("model.headers"))
|
||||
);
|
||||
this.setProperties({
|
||||
headers: plainJSON(headers),
|
||||
body: prettyJSON(this.get("model.payload")),
|
||||
expandDetails: expandDetailsKey,
|
||||
bodyLabel: I18n.t("admin.web_hooks.events.payload"),
|
||||
});
|
||||
} else {
|
||||
this.set("expandDetails", null);
|
||||
}
|
||||
},
|
||||
|
||||
toggleResponse() {
|
||||
const expandDetailsKey = this.expandDetailsResponseKey;
|
||||
|
||||
if (this.expandDetails !== expandDetailsKey) {
|
||||
this.setProperties({
|
||||
headers: plainJSON(this.get("model.response_headers")),
|
||||
body: this.get("model.response_body"),
|
||||
expandDetails: expandDetailsKey,
|
||||
bodyLabel: I18n.t("admin.web_hooks.events.body"),
|
||||
});
|
||||
} else {
|
||||
this.set("expandDetails", null);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
|
||||
export default Component.extend({
|
||||
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
|
||||
icons: ["far-circle", "times-circle", "circle", "circle"],
|
||||
circleIcon: null,
|
||||
deliveryStatus: null,
|
||||
|
||||
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
|
||||
status(deliveryStatuses, lastDeliveryStatus) {
|
||||
return deliveryStatuses.find((s) => s.id === lastDeliveryStatus);
|
||||
},
|
||||
|
||||
@discourseComputed("status.id", "icons")
|
||||
icon(statusId, icons) {
|
||||
return icons[statusId - 1];
|
||||
},
|
||||
|
||||
@discourseComputed("status.id", "classes")
|
||||
class(statusId, classes) {
|
||||
return classes[statusId - 1];
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
this.set(
|
||||
"circleIcon",
|
||||
iconHTML(this.icon, { class: this.class }).htmlSafe()
|
||||
);
|
||||
this.set(
|
||||
"deliveryStatus",
|
||||
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
$("body").addClass("admin-interface");
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
$("body").removeClass("admin-interface");
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
});
|
||||
73
app/assets/javascripts/admin/addon/components/color-input.js
Normal file
@ -0,0 +1,73 @@
|
||||
import { schedule } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import { computed, action } from "@ember/object";
|
||||
import loadScript, { loadCSS } from "discourse/lib/load-script";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
|
||||
/**
|
||||
An input field for a color.
|
||||
|
||||
@param hexValue is a reference to the color's hex value.
|
||||
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
|
||||
@params valid is a boolean indicating if the input field is a valid color.
|
||||
**/
|
||||
export default Component.extend({
|
||||
classNames: ["color-picker"],
|
||||
|
||||
onlyHex: true,
|
||||
|
||||
styleSelection: true,
|
||||
|
||||
maxlength: computed("onlyHex", function () {
|
||||
return this.onlyHex ? 6 : null;
|
||||
}),
|
||||
|
||||
@action
|
||||
onHexInput(color) {
|
||||
this.attrs.onChangeColor && this.attrs.onChangeColor(color || "");
|
||||
},
|
||||
|
||||
@observes("hexValue", "brightnessValue", "valid")
|
||||
hexValueChanged: function () {
|
||||
const hex = this.hexValue;
|
||||
let text = this.element.querySelector("input.hex-input");
|
||||
|
||||
this.attrs.onChangeColor && this.attrs.onChangeColor(hex);
|
||||
|
||||
if (this.valid) {
|
||||
this.styleSelection &&
|
||||
text.setAttribute(
|
||||
"style",
|
||||
"color: " +
|
||||
(this.brightnessValue > 125 ? "black" : "white") +
|
||||
"; background-color: #" +
|
||||
hex +
|
||||
";"
|
||||
);
|
||||
|
||||
if (this.pickerLoaded) {
|
||||
$(this.element.querySelector(".picker")).spectrum({
|
||||
color: "#" + hex,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.styleSelection && text.setAttribute("style", "");
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
loadScript("/javascripts/spectrum.js").then(() => {
|
||||
loadCSS("/javascripts/spectrum.css").then(() => {
|
||||
schedule("afterRender", () => {
|
||||
$(this.element.querySelector(".picker"))
|
||||
.spectrum({ color: "#" + this.hexValue })
|
||||
.on("change.spectrum", (me, color) => {
|
||||
this.set("hexValue", color.toHexString().replace("#", ""));
|
||||
});
|
||||
this.set("pickerLoaded", true);
|
||||
});
|
||||
});
|
||||
});
|
||||
schedule("afterRender", () => this.hexValueChanged());
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,54 @@
|
||||
import I18n from "I18n";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { reads } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
export default Component.extend({
|
||||
editorId: reads("fieldName"),
|
||||
|
||||
@discourseComputed("fieldName")
|
||||
currentEditorMode(fieldName) {
|
||||
return fieldName === "css" ? "scss" : fieldName;
|
||||
},
|
||||
|
||||
@discourseComputed("fieldName", "styles.html", "styles.css")
|
||||
resetDisabled(fieldName) {
|
||||
return (
|
||||
this.get(`styles.${fieldName}`) ===
|
||||
this.get(`styles.default_${fieldName}`)
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("styles", "fieldName")
|
||||
editorContents: {
|
||||
get(styles, fieldName) {
|
||||
return styles[fieldName];
|
||||
},
|
||||
set(value, styles, fieldName) {
|
||||
styles.setField(fieldName, value);
|
||||
return value;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
reset() {
|
||||
bootbox.confirm(
|
||||
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");
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||