Merge master

This commit is contained in:
Neil Lalonde 2020-02-25 17:21:37 -05:00
commit 52c10848bc
7562 changed files with 133059 additions and 55230 deletions

View File

@ -1,21 +0,0 @@
languages:
Ruby: true
JavaScript: true
Python: false
PHP: false
exclude_paths:
- "app/assets/javascripts/defer/*"
- "app/assets/javascripts/ember-addons/*"
- "lib/autospec/*"
- "lib/es6_module_transpiler/*"
- "lib/highlight_js/*"
- "lib/import/*"
- "lib/javascripts/*"
- "lib/tasks/*"
- "lib/*.js"
- "public/*"
- "script/*"
- "spec/*"
- "test/*"
- "vendor/*"

View File

@ -1 +0,0 @@
RAILS_ENV=development

View File

@ -1,96 +1,7 @@
{
"env": {
"browser": true,
"builtin": true,
"es6": true,
"jasmine": true,
"mocha": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module"
},
"globals": {
"$": true,
"_": true,
"andThen": true,
"asyncRender": true,
"Blob": true,
"bootbox": true,
"click": true,
"waitUntil": true,
"getSettledState": true,
"count": true,
"currentPath": true,
"currentRouteName": true,
"currentURL": true,
"define": true,
"Discourse": true,
"Ember": true,
"exists": true,
"File": true,
"fillIn": true,
"find": true,
"Handlebars": true,
"hasModule": true,
"I18n": true,
"invisible": true,
"jQuery": true,
"keyboardHelper": true,
"keyEvent": true,
"moduleFor": true,
"moduleForComponent": true,
"moment": true,
"Pretender": true,
"QUnit": true,
"require": true,
"requirejs": true,
"RSVP": true,
"sandbox": true,
"sinon": true,
"test": true,
"triggerEvent": true,
"visible": true,
"visit": true,
"pauseTest": true
},
"extends": "eslint-config-discourse",
"plugins": ["discourse-ember"],
"rules": {
"block-scoped-var": 2,
"dot-notation": 0,
"eqeqeq": [2, "allow-null"],
"guard-for-in": 2,
"no-alert": 2,
"no-bitwise": 2,
"no-caller": 2,
"no-cond-assign": 0,
"no-console": 2,
"no-debugger": 2,
"no-empty": 0,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-parens": 0,
"no-inner-declarations": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-loop-func": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-str": 2,
"no-new": 2,
"no-plusplus": 0,
"no-proto": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-shadow": 2,
"no-this-before-super": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-unused-vars": 2,
"no-with": 2,
"semi": 2,
"strict": 0,
"valid-typeof": 2,
"wrap-iife": [2, "inside"]
},
"parser": "babel-eslint"
"discourse-ember/global-ember": 2
}
}

9
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,9 @@
# Only add no-op commits to this file
# To prevent these commits to show in git blame
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# DEV: introduces prettier for es6 files
03a7d532cf8f09b12573b21ef013c21100d52728
# DEV: enforces no self-closing-void-elements
dafd3c3b47f116c6c1dc56cb18df614c11747733

158
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,158 @@
name: CI
on:
push:
branches:
- master
pull_request:
branches-ignore:
- 'tests-passed'
jobs:
build:
name: "${{ matrix.target }}-${{ matrix.build_types }}"
runs-on: ${{ matrix.os }}
timeout-minutes: 60
env:
DISCOURSE_HOSTNAME: www.example.com
RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072
BUILD_TYPE: ${{ matrix.build_types }}
TARGET: ${{ matrix.target }}
RAILS_ENV: test
PGHOST: localhost
PGUSER: discourse
PGPASSWORD: discourse
strategy:
fail-fast: false
matrix:
build_types: [ 'BACKEND', 'FRONTEND', 'LINT' ]
target: [ 'PLUGINS', 'CORE' ]
os: [ ubuntu-latest ]
ruby: [ '2.6' ]
postgres: [ '10' ]
redis: [ '4.x' ]
services:
postgres:
image: postgres:${{ matrix.postgres }}
ports:
- 5432:5432
env:
POSTGRES_USER: discourse
POSTGRES_PASSWORD: discourse
POSTGRES_DB: discourse_test
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@master
with:
fetch-depth: 1
- name: Setup Git
run: git config --global user.email "ci@ci.invalid" && git config --global user.name "Discourse CI"
- name: Setup packages
if: env.BUILD_TYPE != 'LINT'
run: |
sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead && \
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- name: Setup redis
uses: shogo82148/actions-setup-redis@v1
if: env.BUILD_TYPE != 'LINT'
with:
redis-version: ${{ matrix.redis }}
- name: Setup ruby
uses: actions/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
architecture: 'x64'
- name: Setup bundler
run: gem install bundler -v 2.1.1 --no-doc
- name: Bundler cache
uses: actions/cache@v1
id: bundler-cache
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Setup gems
run: bundle install --without development --deployment --jobs 4 --retry 3
- name: Get yarn cache directory
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
uses: actions/cache@v1
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.os }}-yarn-
- name: Yarn install
run: yarn install --dev
- name: "Checkout official plugins"
if: env.TARGET == 'PLUGINS'
run: bin/rake plugin:install_all_official
- name: Create database
if: env.BUILD_TYPE != 'LINT'
run: bin/rake db:create && bin/rake db:migrate
- name: Create parallel databases
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'
run: bin/rake parallel:create && bin/rake parallel:migrate
- name: Rubocop
if: env.BUILD_TYPE == 'LINT'
run: bundle exec rubocop .
- name: ESLint
if: env.BUILD_TYPE == 'LINT'
run: yarn eslint app/assets/javascripts test/javascripts && yarn eslint --ext .es6 app/assets/javascripts test/javascripts plugins
- name: Prettier
if: env.BUILD_TYPE == 'LINT'
run: |
yarn prettier -v
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
- name: Core RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'
run: bin/turbo_rspec && bin/rake plugin:spec
- name: Plugin RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'PLUGINS'
run: bin/rake plugin:spec
- name: Core QUnit
if: env.BUILD_TYPE == 'FRONTEND' && env.TARGET == 'CORE'
run: bundle exec rake qunit:test['1200000']
timeout-minutes: 30
- name: Wizard QUnit
if: env.BUILD_TYPE == 'FRONTEND' && env.TARGET == 'CORE'
run: bundle exec rake qunit:test['1200000','/wizard/qunit']
timeout-minutes: 30
- name: Plugin QUnit # Tests core plugins in TARGET=CORE, and all plugins in TARGET=PLUGINS
if: env.BUILD_TYPE == 'FRONTEND'
run: bundle exec rake plugin:qunit
timeout-minutes: 30

8
.gitignore vendored
View File

@ -32,6 +32,7 @@ config/discourse.conf
# Ignore the default SQLite database and db dumps
*.sql
*.sql.gz
!/spec/fixtures/**/*.sql
/db/*.sqlite3
/db/structure.sql
/db/schema.rb
@ -46,14 +47,14 @@ bootsnap-compile-cache/
# Ignore plugins except for the bundled ones.
/plugins/*
!/plugins/lazyYT/
!/plugins/lazy-yt/
!/plugins/poll/
!/plugins/discourse-details/
!/plugins/discourse-nginx-performance-report
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/discourse-local-dates
!/public/plugins/discourse-internet-explorer
!/plugins/discourse-internet-explorer
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated
@ -128,3 +129,6 @@ vendor/bundle/*
# Vagrant
.vagrant
# ignore auto-generated plugin js assets
/app/assets/javascripts/plugins/*

View File

@ -1,45 +0,0 @@
# Use this file to configure the Overcommit hooks you wish to use. This will
# extend the default configuration defined in:
# https://github.com/brigade/overcommit/blob/master/config/default.yml
#
# At the topmost level of this YAML file is a key representing type of hook
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
# customize each hook, such as whether to only run it on certain files (via
# `include`), whether to only display output if it fails (via `quiet`), etc.
#
# For a complete list of hooks, see:
# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
#
# For a complete list of options that you can use to customize hooks, see:
# https://github.com/brigade/overcommit#configuration
PreCommit:
RuboCop:
enabled: true
command: ['bundle', 'exec', 'rubocop']
EsLint:
enabled: true
required_executable: './node_modules/.bin/eslint'
install_command: 'yarn install'
command: ['yarn', 'eslint', '--ext', '.es6', '-f', 'compact']
include: '**/*.es6'
YamlSyntax:
enabled: true
PostCheckout:
BundleInstall:
enabled: true
YarnInstall:
enabled: true
PostMerge:
BundleInstall:
enabled: true
YarnInstall:
enabled: true
PostRewrite:
BundleInstall:
enabled: true
YarnInstall:
enabled: true

View File

@ -1 +0,0 @@
before_precompile: ./packaging/debian/setup.sh

View File

@ -1,13 +1,16 @@
require:
- rubocop-discourse
AllCops:
TargetRubyVersion: 2.4
DisabledByDefault: true
Exclude:
- 'db/schema.rb'
- 'bundle/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'public/**/*'
- 'plugins/**/*'
- "db/schema.rb"
- "bundle/**/*"
- "vendor/**/*"
- "node_modules/**/*"
- "public/**/*"
- "plugins/**/gems/**/*"
# Prefer &&/|| over and/or.
Style/AndOr:
@ -16,11 +19,6 @@ Style/AndOr:
Style/FrozenStringLiteralComment:
Enabled: true
# Do not use braces for hash literals when they are the last argument of a
# method call.
Style/BracesAroundHashParameters:
Enabled: true
# Align `when` with `case`.
Layout/CaseIndentation:
Enabled: true
@ -57,7 +55,7 @@ Layout/SpaceAroundOperators:
Enabled: true
Layout/SpaceBeforeFirstArg:
Enabled: true
Enabled: true
# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
@ -83,7 +81,7 @@ Layout/Tab:
Enabled: true
# Blank lines should not have any spaces.
Layout/TrailingBlankLines:
Layout/TrailingEmptyLines:
Enabled: true
# No trailing whitespace.
@ -113,7 +111,7 @@ Layout/MultilineMethodCallIndentation:
Enabled: true
EnforcedStyle: indented
Layout/AlignHash:
Layout/HashAlignment:
Enabled: true
Bundler/OrderedGems:
@ -126,6 +124,18 @@ Style/Semicolon:
Enabled: true
AllowAsExpressionSeparator: true
Style/RedundantReturn:
Enabled: true
DiscourseCops/NoChdir:
Enabled: true
Exclude:
- 'spec/**/*' # Specs are run sequentially, so chdir can be used
- 'plugins/*/spec/**/*'
DiscourseCops/NoURIEscapeEncode:
Enabled: true
Style/GlobalVars:
Enabled: true
Severity: warning

View File

@ -1 +1 @@
2.6.1
2.6.5

11
.template-lintrc.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
// extends: "recommended",
ignore: ["**/*.raw"],
rules: {
"self-closing-void-elements": true,
"table-groups": true,
"style-concatenation": true,
"no-invalid-interactive": true
}
};

View File

@ -21,7 +21,7 @@ env:
addons:
chrome: stable
postgresql: 9.6
postgresql: "9.6"
apt:
update: true
packages:
@ -40,10 +40,9 @@ services:
- redis-server
sudo: required
dist: trusty
dist: xenial
cache:
apt: true
yarn: true
directories:
- vendor/bundle
@ -60,7 +59,7 @@ before_install:
- git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration
- git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign
- git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon
- git clone --depth=1 https://github.com/discourse/discourse-staff-notes.git plugins/discourse-staff-notes
- git clone --depth=1 https://github.com/discourse/discourse-user-notes.git plugins/discourse-user-notes
- git clone --depth=1 https://github.com/discourse/discourse-group-tracker
- export PATH=$HOME/.yarn/bin:$PATH
@ -74,13 +73,7 @@ script:
- |
bash -c "
if [ '$RUN_LINT' == '1' ]; then
bundle exec rubocop --parallel && \
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"
yarn eslint --ext .es6 app/assets/javascripts && \
yarn eslint --ext .es6 test/javascripts && \
yarn eslint --ext .es6 plugins/**/assets/javascripts && \
yarn eslint --ext .es6 plugins/**/test/javascripts && \
yarn eslint app/assets/javascripts test/javascripts
npx lefthook run lints
else
if [ '$QUNIT_RUN' == '1' ]; then
bundle exec rake qunit:test['1200000'] && \

125
Gemfile
View File

@ -14,45 +14,60 @@ if rails_master?
gem 'arel', git: 'https://github.com/rails/arel.git'
gem 'rails', git: 'https://github.com/rails/rails.git'
else
# until rubygems gives us optional dependencies we are stuck with this
# bundle update actionmailer actionpack actionview activemodel activerecord activesupport railties
gem 'actionmailer', '5.2.3'
gem 'actionpack', '5.2.3'
gem 'actionview', '5.2.3'
gem 'activemodel', '5.2.3'
gem 'activerecord', '5.2.3'
gem 'activesupport', '5.2.3'
gem 'railties', '5.2.3'
# NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
gem 'actionmailer', '6.0.1'
gem 'actionpack', '6.0.1'
gem 'actionview', '6.0.1'
gem 'activemodel', '6.0.1'
gem 'activerecord', '6.0.1'
gem 'activesupport', '6.0.1'
gem 'railties', '6.0.1'
gem 'sprockets-rails'
end
# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
# This is a desired upgrade we should get to.
gem 'sprockets', '3.7.2'
# this will eventually be added to rails,
# allows us to precompile all our templates in the unicorn master
gem 'actionview_precompiler', require: false
gem 'seed-fu'
gem 'mail', require: false
gem 'mini_mime'
gem 'mini_suffix'
gem 'hiredis'
gem 'redis'
# holding off redis upgrade temporarily as it is having issues with our current
# freedom patch, we will follow this up.
#
# FrozenError: can't modify frozen Hash
# /var/www/discourse/vendor/bundle/ruby/2.5.0/gems/redis-4.1.0/lib/redis/client.rb:93:in `delete'
# /var/www/discourse/vendor/bundle/ruby/2.5.0/gems/redis-4.1.0/lib/redis/client.rb:93:in `initialize'
# /var/www/discourse/lib/freedom_patches/redis.rb:7:in `initialize'
gem 'redis', '4.0.1', require: ["redis", "redis/connection/hiredis"]
# This is explicitly used by Sidekiq and is an optional dependency.
# We tell Sidekiq to use the namespace "sidekiq" which triggers this
# gem to be used. There is no explicit dependency in sidekiq cause
# redis namespace support is optional
# We already namespace stuff in DiscourseRedis, so we should consider
# just using a single implementation in core vs having 2 namespace implementations
gem 'redis-namespace'
# NOTE: AM serializer gets a lot slower with recent updates
# we used an old branch which is the fastest one out there
# are long term goal here is to fork this gem so we have a
# better maintained living fork
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.92'
gem 'onebox'
gem 'http_accept_language', '~>2.0.5', require: false
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-source', '~> 3.8.0'
gem 'discourse-ember-source', '~> 3.12.2'
gem 'ember-handlebars-template', '0.8.0'
gem 'barber'
gem 'message_bus'
@ -71,17 +86,17 @@ gem 'aws-sdk-sns', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_trimmer', '~> 0.1'
gem 'email_reply_trimmer'
# Forked until https://github.com/toy/image_optim/pull/162 is merged
# https://github.com/discourse/image_optim
gem 'discourse_image_optim', require: 'image_optim'
gem 'multi_json'
gem 'mustache'
gem 'nokogiri'
gem 'css_parser', require: false
gem 'omniauth'
gem 'omniauth-openid'
gem 'openid-redis-store'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-instagram'
@ -95,7 +110,7 @@ gem 'oj'
gem 'pg'
gem 'mini_sql'
gem 'pry-rails', require: false
gem 'r2', '~> 0.2.5', require: false
gem 'r2', require: false
gem 'rake'
gem 'thor', require: false
@ -110,8 +125,20 @@ gem 'tilt', require: false
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
# TODO: Upgrading breaks Sidekiq Web
# This is a bit of a hornets nest cause in an ideal world we much prefer
# if Sidekiq reused session and CSRF mitigation with Discourse on the
# _forum_session cookie instead of a rack.session cookie
gem 'rack', '2.0.8'
gem 'rack-protection' # security
gem 'cbor', require: false
gem 'cose', require: false
gem 'addressable'
# Gems used only for assets and not required in production environments by default.
# Allow everywhere for now cause we are allowing asset debugging in production
@ -122,7 +149,7 @@ end
group :test do
gem 'webmock', require: false
gem 'fakeweb', '~> 1.3.0', require: false
gem 'fakeweb', require: false
gem 'minitest', require: false
gem 'simplecov', require: false
gem "test-prof"
@ -133,32 +160,37 @@ group :test, :development do
gem 'mock_redis'
gem 'listen', require: false
gem 'certified', require: false
# later appears to break Fabricate(:topic, category: category)
gem 'fabrication', require: false
gem 'mocha', require: false
# TODO: upgrading to 1.10.1 cause it breaks our test suite.
# We want our test suite fixed though to support this upgrade.
gem 'mocha', '1.8.0', require: false
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', require: false
gem 'shoulda-matchers', '~> 3.1', '>= 3.1.3', require: false
# TODO once 4.0.0 is released upgrade to it, at time of writing 3.9.0 is latest
gem 'rspec-rails', '4.0.0.beta2', require: false
gem 'shoulda-matchers', require: false
gem 'rspec-html-matchers'
gem 'pry-nav'
gem 'byebug', require: ENV['RM_INFO'].nil?
gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri
gem 'rubocop', require: false
gem "rubocop-discourse", require: false
gem 'parallel_tests'
end
group :development do
gem 'ruby-prof', require: false
gem 'ruby-prof', require: false, platform: :mri
gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors'
gem 'better_errors', platform: :mri
gem 'binding_of_caller'
# waiting on 2.7.5 per: https://github.com/ctran/annotate_models/pull/595
if rails_master?
gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
else
gem 'annotate'
end
gem 'yaml-lint'
gem 'annotate'
end
# this is an optional gem, it provides a high performance replacement
@ -196,24 +228,31 @@ gem 'logstash-event', require: false
gem 'logstash-logger', require: false
gem 'logster'
gem 'sassc', require: false
# NOTE: later versions of sassc are causing a segfault, possibly dependent on processer architecture
# and until resolved should be locked at 2.0.1
gem 'sassc', '2.0.1', require: false
gem "sassc-rails"
gem 'rotp'
gem 'rotp', require: false
gem 'rqrcode'
gem 'rubyzip', require: false
gem 'sshkey', require: false
gem 'rchardet', require: false
gem 'lz4-ruby', require: false, platform: :mri
if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'
# NOTE: in import mode the version of sqlite can matter a lot, so we stick it to a specific one
gem 'sqlite3', '~> 1.3', '>= 1.3.13'
gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md'
gem 'reverse_markdown'
gem 'tiny_tds'
gem 'csv', '~> 3.0'
gem 'csv'
end
gem 'webpush', require: false

View File

@ -1,64 +1,65 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
actionmailer (6.0.1)
actionpack (= 6.0.1)
actionview (= 6.0.1)
activejob (= 6.0.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.3)
actionview (= 5.2.3)
activesupport (= 5.2.3)
actionpack (6.0.1)
actionview (= 6.0.1)
activesupport (= 6.0.1)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.3)
activesupport (= 5.2.3)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (6.0.1)
activesupport (= 6.0.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
actionview_precompiler (0.2.2)
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (5.2.3)
activesupport (= 5.2.3)
activejob (6.0.1)
activesupport (= 6.0.1)
globalid (>= 0.3.6)
activemodel (5.2.3)
activesupport (= 5.2.3)
activerecord (5.2.3)
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0)
activesupport (5.2.3)
activemodel (6.0.1)
activesupport (= 6.0.1)
activerecord (6.0.1)
activemodel (= 6.0.1)
activesupport (= 6.0.1)
activesupport (6.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
annotate (2.7.5)
zeitwerk (~> 2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
annotate (3.1.0)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 13.0)
arel (9.0.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
aws-eventstream (1.0.3)
aws-partitions (1.154.0)
aws-sdk-core (3.48.6)
aws-partitions (1.272.0)
aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.17.0)
aws-sdk-core (~> 3, >= 3.48.2)
aws-sdk-kms (1.29.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.36.1)
aws-sdk-core (~> 3, >= 3.48.2)
aws-sdk-s3 (1.60.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sdk-sns (1.13.0)
aws-sdk-core (~> 3, >= 3.48.2)
aws-sigv4 (~> 1.1)
aws-sdk-sns (1.21.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
aws-eventstream (~> 1.0, >= 1.0.2)
@ -71,34 +72,40 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.4.4)
bootsnap (1.4.6)
msgpack (~> 1.0)
builder (3.2.3)
bullet (6.0.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.0.1)
byebug (11.1.1)
cbor (0.5.9.6)
certified (1.0.0)
chunky_png (1.3.11)
coderay (1.1.2)
colored2 (3.1.2)
concurrent-ruby (1.1.5)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
cose (0.11.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.3.0)
cppjieba_rb (0.3.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
crass (1.0.6)
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
diff-lcs (1.3)
diffy (3.3.0)
discourse-ember-source (3.8.0.1)
discourse-ember-source (3.12.2.0)
discourse_image_optim (0.26.2)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
docile (1.3.1)
docile (1.3.2)
email_reply_trimmer (0.1.12)
ember-data-source (3.0.2)
ember-source (>= 2, < 3.0)
@ -113,53 +120,50 @@ GEM
jquery-rails (>= 1.0.17)
railties (>= 3.1)
ember-source (2.18.2)
erubi (1.8.0)
excon (0.64.0)
erubi (1.9.0)
excon (0.72.0)
execjs (2.7.0)
exifr (1.3.6)
fabrication (2.20.1)
fabrication (2.21.0)
fakeweb (1.3.0)
faraday (0.15.4)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fast_xor (1.1.3)
rake
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.5)
ffi (1.10.0)
fastimage (2.1.7)
ffi (1.12.2)
flamegraph (0.9.5)
fspath (3.1.0)
fspath (3.1.2)
gc_tracer (1.5.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
guess_html_encoding (0.0.11)
hashdiff (0.3.9)
hashdiff (1.0.0)
hashie (3.6.0)
highline (1.7.10)
hiredis (0.6.3)
hkdf (0.3.0)
htmlentities (4.3.4)
http_accept_language (2.0.5)
i18n (1.6.0)
http_accept_language (2.1.1)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
image_size (1.5.0)
in_threads (1.5.1)
jaro_winkler (1.5.2)
in_threads (1.5.4)
jaro_winkler (1.5.4)
jmespath (1.4.0)
jquery-rails (4.3.3)
jquery-rails (4.3.5)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.2.0)
jwt (2.2.1)
kgio (2.11.2)
kgio (2.11.3)
libv8 (7.3.492.27.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
lograge (0.11.0)
listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lograge (0.11.2)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@ -167,62 +171,63 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.3.2)
loofah (2.2.3)
logster (2.6.3)
loofah (2.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
lz4-ruby (0.3.3)
mail (2.7.1)
mini_mime (>= 0.1.1)
maxminddb (0.1.22)
memory_profiler (0.9.13)
message_bus (2.2.0)
memory_profiler (0.9.14)
message_bus (2.2.3)
rack (>= 1.1.3)
metaclass (0.0.4)
method_source (0.9.2)
mini_mime (1.0.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_racer (0.2.6)
mini_racer (0.2.9)
libv8 (>= 6.9.411)
mini_scheduler (0.11.0)
mini_scheduler (0.12.2)
sidekiq
mini_sql (0.2.2)
mini_sql (0.2.4)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.11.3)
minitest (5.14.0)
mocha (1.8.0)
metaclass (~> 0.0.1)
mock_redis (0.19.0)
moneta (1.1.1)
msgpack (1.2.10)
multi_json (1.13.1)
mock_redis (0.22.0)
msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
mustache (1.1.0)
nokogiri (1.10.3)
mustache (1.1.1)
nio4r (2.5.2)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.1)
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
oauth (0.5.4)
oauth2 (1.4.1)
faraday (>= 0.8, < 0.16.0)
oauth2 (1.4.2)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.7.12)
oj (3.10.2)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3)
omniauth-facebook (5.0.0)
omniauth-facebook (6.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.4.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-google-oauth2 (0.7.0)
omniauth-google-oauth2 (0.8.0)
jwt (>= 2.0)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
omniauth-oauth2 (>= 1.6)
omniauth-instagram (1.3.0)
omniauth (~> 1)
omniauth-oauth2 (~> 1)
@ -232,30 +237,25 @@ GEM
omniauth-oauth2 (1.6.0)
oauth2 (~> 1.1)
omniauth (~> 1.9)
omniauth-openid (1.0.1)
omniauth (~> 1.0)
rack-openid (~> 1.3.1)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.92)
onebox (1.9.26)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
openid-redis-store (0.0.2)
redis
ruby-openid
openssl-signature_algorithm (0.3.0)
optimist (3.0.0)
parallel (1.17.0)
parallel_tests (2.28.0)
parallel (1.19.1)
parallel_tests (2.31.0)
parallel
parser (2.6.3.0)
parser (2.7.0.2)
ast (~> 2.4.0)
pg (1.1.4)
progress (3.5.0)
pg (1.2.2)
progress (3.5.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@ -263,103 +263,107 @@ GEM
pry (>= 0.9.10, < 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (3.0.3)
puma (3.12.1)
public_suffix (4.0.3)
puma (4.3.1)
nio4r (~> 2.0)
r2 (0.2.7)
rack (2.0.7)
rack-mini-profiler (1.1.4)
rack (2.0.8)
rack-mini-profiler (1.1.6)
rack (>= 1.2.0)
rack-openid (1.3.1)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-protection (2.0.5)
rack-protection (2.0.8.1)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_multisite (2.0.7)
activerecord (> 4.2, < 7)
railties (> 4.2, < 7)
railties (5.2.3)
actionpack (= 5.2.3)
activesupport (= 5.2.3)
railties (6.0.1)
actionpack (= 6.0.1)
activesupport (= 6.0.1)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
raindrops (0.19.0)
rake (12.3.2)
rake-compiler (1.0.7)
raindrops (0.19.1)
rake (13.0.1)
rake-compiler (1.1.0)
rake
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtrace (0.4.11)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.0.1)
redis-namespace (1.6.0)
redis (4.1.3)
redis-namespace (1.7.0)
redis (>= 3.0.4)
request_store (1.4.1)
request_store (1.5.0)
rack (>= 1.4)
rexml (3.2.4)
rinku (2.0.6)
rotp (3.3.1)
rqrcode (0.10.1)
rotp (5.1.0)
addressable (~> 2.5)
rqrcode (1.1.2)
chunky_png (~> 1.0)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-core (3.8.0)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.3)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.1)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-html-matchers (0.9.1)
rspec-support (~> 3.9.0)
rspec-html-matchers (0.9.2)
nokogiri (~> 1)
rspec (>= 3.0.0.a, < 4)
rspec-mocks (3.8.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-rails (3.8.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta2)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.9.2)
rtlit (0.0.5)
rubocop (0.69.0)
rubocop (0.80.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
ruby-openid (2.7.0)
ruby-prof (0.17.0)
ruby-progressbar (1.10.0)
rubocop-discourse (1.0.2)
rubocop (>= 0.69.0)
ruby-prof (1.3.0)
ruby-progressbar (1.10.1)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
ruby_dep (1.5.0)
rubyzip (2.2.0)
safe_yaml (1.0.5)
sanitize (5.0.0)
sanitize (5.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
sassc (2.0.1)
ffi (~> 1.9)
rake
sassc-rails (2.1.1)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
@ -368,18 +372,17 @@ GEM
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
shoulda-matchers (3.1.3)
activesupport (>= 4.0.0)
sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
simplecov (0.16.1)
shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
sidekiq (6.0.5)
connection_pool (>= 2.2.2)
rack (~> 2.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
simplecov (0.18.3)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
simplecov-html (~> 0.11)
simplecov-html (0.12.1)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -388,42 +391,46 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.12)
test-prof (0.9.0)
thor (0.20.3)
stackprof (0.2.15)
test-prof (0.11.3)
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.9)
tzinfo (1.2.5)
tilt (2.0.10)
tzinfo (1.2.6)
thread_safe (~> 0.1)
uglifier (4.1.20)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
unicorn (5.5.1)
unicode-display_width (1.6.1)
unicorn (5.5.3)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.12.1)
webmock (3.5.1)
uniform_notifier (1.13.0)
webmock (3.8.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpush (0.3.8)
hashdiff (>= 0.4.0, < 2.0.0)
webpush (1.0.0)
hkdf (~> 0.2)
jwt (~> 2.0)
yaml-lint (0.0.10)
zeitwerk (2.2.2)
PLATFORMS
ruby
DEPENDENCIES
actionmailer (= 5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
actionmailer (= 6.0.1)
actionpack (= 6.0.1)
actionview (= 6.0.1)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 5.2.3)
activerecord (= 5.2.3)
activesupport (= 5.2.3)
activemodel (= 6.0.1)
activerecord (= 6.0.1)
activesupport (= 6.0.1)
addressable
annotate
aws-sdk-s3
aws-sdk-sns
@ -433,19 +440,22 @@ DEPENDENCIES
bootsnap
bullet
byebug
cbor
certified
colored2
cose
cppjieba_rb
css_parser
diffy
discourse-ember-source (~> 3.8.0)
discourse-ember-source (~> 3.12.2)
discourse_image_optim
email_reply_trimmer (~> 0.1)
email_reply_trimmer
ember-handlebars-template (= 0.8.0)
ember-rails (= 0.18.5)
excon
execjs
fabrication
fakeweb (~> 1.3.0)
fakeweb
fast_blank
fast_xor
fast_xs
@ -453,15 +463,15 @@ DEPENDENCIES
flamegraph
gc_tracer
highline (~> 1.7.0)
hiredis
htmlentities
http_accept_language (~> 2.0.5)
http_accept_language
listen
lograge
logstash-event
logstash-logger
logster
lru_redux
lz4-ruby
mail
maxminddb
memory_profiler
@ -472,7 +482,7 @@ DEPENDENCIES
mini_sql
mini_suffix
minitest
mocha
mocha (= 1.8.0)
mock_redis
multi_json
mustache
@ -484,44 +494,46 @@ DEPENDENCIES
omniauth-google-oauth2
omniauth-instagram
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.92)
openid-redis-store
onebox
parallel_tests
pg
pry-nav
pry-rails
puma
r2 (~> 0.2.5)
r2
rack (= 2.0.8)
rack-mini-profiler
rack-protection
rails_multisite
railties (= 5.2.3)
railties (= 6.0.1)
rake
rb-fsevent
rb-inotify (~> 0.9)
rbtrace
rchardet
redis (= 4.0.1)
redis
redis-namespace
rinku
rotp
rqrcode
rspec
rspec-html-matchers
rspec-rails
rspec-rails (= 4.0.0.beta2)
rtlit
rubocop
rubocop-discourse
ruby-prof
ruby-readability
rubyzip
sanitize
sassc
sassc (= 2.0.1)
sassc-rails
seed-fu
shoulda-matchers (~> 3.1, >= 3.1.3)
shoulda-matchers
sidekiq
simplecov
sprockets (= 3.7.2)
sprockets-rails
sshkey
stackprof
@ -533,6 +545,7 @@ DEPENDENCIES
unicorn
webmock
webpush
yaml-lint
BUNDLED WITH
2.1.1

View File

@ -44,14 +44,16 @@ If you're looking for business class hosting, see [discourse.org/buy](https://ww
## Requirements
Discourse is built for the *next* 10 years of the Internet, so our requirements are high:
Discourse is built for the *next* 10 years of the Internet, so our requirements are high.
Discourse supports the **latest, stable releases** of all major browsers and platforms:
| Browsers | Tablets | Phones |
| --------------------- | ------------ | ------------ |
| Safari 10+ | iPad 4+ | iOS 10+ |
| Google Chrome 57+ | Android 4.4+ | Android 4.4+ |
| Internet Explorer 11+ | | |
| Firefox 52+ | | |
| Apple Safari | iPadOS | iOS |
| Google Chrome | Android | Android |
| Microsoft Edge | | |
| Mozilla Firefox | | |
## Built With

View File

@ -0,0 +1,11 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
apiNameFor() {
return "key";
}
});

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/email_style";
}
});

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/logs/";
}
});

View File

@ -0,0 +1,5 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
jsonMode: true
});

View File

@ -1,7 +1,9 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import { observes } from "ember-addons/ember-computed-decorators";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
export default Ember.Component.extend({
export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
@ -17,8 +19,9 @@ export default Ember.Component.extend({
@observes("content")
contentChanged() {
if (this._editor && !this._skipContentChangeEvent && this.content) {
this._editor.getSession().setValue(this.content);
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},
@ -47,7 +50,7 @@ export default Ember.Component.extend({
}
},
_destroyEditor: function() {
_destroyEditor: on("willDestroyElement", function() {
if (this._editor) {
this._editor.destroy();
this._editor = null;
@ -58,7 +61,7 @@ export default Ember.Component.extend({
}
$(window).off("ace:resize");
}.on("willDestroyElement"),
}),
resize() {
if (this._editor) {
@ -74,7 +77,7 @@ export default Ember.Component.extend({
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.$(".ace")[0]);
const editor = loadedAce.edit(this.element.querySelector(".ace"));
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
@ -88,7 +91,7 @@ export default Ember.Component.extend({
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.$().data("editor", editor);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();

View File

@ -1,73 +1,73 @@
import debounce from "discourse/lib/debounce";
import { renderSpinner } from "discourse/helpers/loading-spinner";
import { escapeExpression } from "discourse/lib/utilities";
import { bufferedRender } from "discourse-common/lib/buffered-render";
import { observes, on } from "ember-addons/ember-computed-decorators";
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 Ember.Component.extend(
bufferedRender({
classNames: ["admin-backups-logs"],
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();
},
init() {
this._super(...arguments);
this._reset();
},
_reset() {
this.setProperties({ formattedLogs: "", index: 0 });
},
_reset() {
this.setProperties({ formattedLogs: "", index: 0 });
},
_scrollDown() {
const $div = this.$()[0];
$div.scrollTop = $div.scrollHeight;
},
_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.rerenderBuffer();
}
},
@on("init")
@observes("logs.[]")
_updateFormattedLogs: debounce(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 = escapeExpression(logs[i].get("message"));
formattedLogs += "[" + date + "] " + message + "\n";
}
// update the formatted logs & cache index
this.setProperties({
formattedLogs: formattedLogs,
index: logs.length
});
// force rerender
this.rerenderBuffer();
Ember.run.scheduleOnce("afterRender", this, this._scrollDown);
}, 150),
buildBuffer(buffer) {
const formattedLogs = this.formattedLogs;
if (formattedLogs && formattedLogs.length > 0) {
buffer.push("<pre>");
buffer.push(formattedLogs);
buffer.push("</pre>");
} else {
buffer.push("<p>" + I18n.t("admin.backups.logs.none") + "</p>");
}
// add a loading indicator
if (this.get("status.isOperationRunning")) {
buffer.push(renderSpinner("small"));
}
@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);
}
}
});

View File

@ -1,35 +1,30 @@
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import { bufferedRender } from "discourse-common/lib/buffered-render";
export default Ember.Component.extend(
bufferedRender({
tagName: "th",
classNames: ["sortable"],
rerenderTriggers: ["order", "ascending"],
buildBuffer(buffer) {
const icon = this.icon;
if (icon) {
buffer.push(iconHTML(icon));
}
buffer.push(I18n.t(this.i18nKey));
if (this.field === this.order) {
buffer.push(iconHTML(this.ascending ? "chevron-up" : "chevron-down"));
}
},
click() {
const currentOrder = this.order;
const field = this.field;
if (currentOrder === field) {
this.set("ascending", this.ascending ? null : true);
} else {
this.setProperties({ order: field, ascending: null });
}
export default Component.extend({
tagName: "th",
classNames: ["sortable"],
chevronIcon: null,
toggleProperties() {
if (this.order === this.field) {
this.set("ascending", this.ascending ? null : true);
} else {
this.setProperties({ order: this.field, ascending: null });
}
})
);
},
toggleChevron() {
if (this.order === this.field) {
let chevron = iconHTML(this.ascending ? "chevron-up" : "chevron-down");
this.set("chevronIcon", `${chevron}`.htmlSafe());
} else {
this.set("chevronIcon", null);
}
},
click() {
this.toggleProperties();
},
didReceiveAttrs() {
this._super(...arguments);
this.toggleChevron();
}
});

View File

@ -1,4 +1,5 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: "",
buffer: "",

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
classNames: ["row"]
});

View File

@ -1,11 +1,12 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
export default Ember.Component.extend({
export default Component.extend({
tagName: "canvas",
type: "line",
refreshChart() {
const ctx = this.$()[0].getContext("2d");
const ctx = this.element.getContext("2d");
const model = this.model;
const rawData = this.get("model.data");

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: ""
});

View File

@ -1,7 +1,11 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce } from "@ember/runloop";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
@ -10,7 +14,7 @@ export default Ember.Component.extend({
this._super(...arguments);
this.resizeHandler = () =>
Ember.run.debounce(this, this._scheduleChartRendering, 500);
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
@ -30,23 +34,24 @@ export default Ember.Component.extend({
didReceiveAttrs() {
this._super(...arguments);
Ember.run.debounce(this, this._scheduleChartRendering, 100);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
Ember.run.schedule("afterRender", () => {
this._renderChart(this.model, this.$(".chart-canvas"));
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, $chartCanvas) {
if (!$chartCanvas || !$chartCanvas.length) return;
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = $chartCanvas[0].getContext("2d");
const chartData = Ember.makeArray(
model.get("chartData") || model.get("data")
);
const prevChartData = Ember.makeArray(
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
@ -82,6 +87,11 @@ export default Ember.Component.extend({
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
@ -102,6 +112,10 @@ export default Ember.Component.extend({
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0
},
layout: {
padding: {
left: 0,
@ -118,7 +132,10 @@ export default Ember.Component.extend({
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label)
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
@ -129,6 +146,11 @@ export default Ember.Component.extend({
type: "time",
time: {
parser: "YYYY-MM-DD"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]

View File

@ -1,4 +1,5 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-counters"],
attributeBindings: ["model.description:title"]

View File

@ -1,7 +1,9 @@
export default Ember.Component.extend({
import { match } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
allTime: true,
tagName: "tr",
reverseColors: Ember.computed.match(
reverseColors: match(
"report.type",
/^(time_to_first_response|topics_with_no_response)$/
),

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-inline-table"]
});

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: "tr"
});

View File

@ -1,14 +1,18 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce } from "@ember/runloop";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
init() {
this._super(...arguments);
this.resizeHandler = () =>
Ember.run.debounce(this, this._scheduleChartRendering, 500);
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
@ -28,26 +32,31 @@ export default Ember.Component.extend({
didReceiveAttrs() {
this._super(...arguments);
Ember.run.debounce(this, this._scheduleChartRendering, 100);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
Ember.run.schedule("afterRender", () => {
this._renderChart(this.model, this.$(".chart-canvas"));
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, $chartCanvas) {
if (!$chartCanvas || !$chartCanvas.length) return;
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = $chartCanvas[0].getContext("2d");
const context = chartCanvas.getContext("2d");
const chartData = Ember.makeArray(
model.get("chartData") || model.get("data")
);
const chartData = makeArray(model.get("chartData") || model.get("data"));
const data = {
labels: chartData[0].data.map(cd => cd.x),
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map(cd => {
return {
label: cd.label,
@ -60,6 +69,7 @@ export default Ember.Component.extend({
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
@ -71,7 +81,11 @@ export default Ember.Component.extend({
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0
},
tooltips: {
mode: "index",
intersect: false,
@ -87,7 +101,6 @@ export default Ember.Component.extend({
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
legend: { display: false },
layout: {
padding: {
left: 0,
@ -105,7 +118,10 @@ export default Ember.Component.extend({
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label)
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
@ -118,6 +134,11 @@ export default Ember.Component.extend({
time: {
parser: "YYYY-MM-DD",
minUnit: "day"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]

View File

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

View File

@ -1,18 +1,20 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Ember.Component.extend({
export default Component.extend({
tagName: "td",
classNames: ["admin-report-table-cell"],
classNameBindings: ["type", "property"],
options: null,
@computed("label", "data", "options")
@discourseComputed("label", "data", "options")
computedLabel(label, data, options) {
return label.compute(data, options || {});
},
type: Ember.computed.alias("label.type"),
property: Ember.computed.alias("label.mainProperty"),
formatedValue: Ember.computed.alias("computedLabel.formatedValue"),
value: Ember.computed.alias("computedLabel.value")
type: alias("label.type"),
property: alias("label.mainProperty"),
formatedValue: alias("computedLabel.formatedValue"),
value: alias("computedLabel.value")
});

View File

@ -1,17 +1,18 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Ember.Component.extend({
export default Component.extend({
tagName: "th",
classNames: ["admin-report-table-header"],
classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
attributeBindings: ["label.title:title"],
@computed("currentSortLabel.sortProperty", "label.sortProperty")
@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
isCurrentSort(currentSortField, labelSortField) {
return currentSortField === labelSortField;
},
@computed("currentSortDirection")
@discourseComputed("currentSortDirection")
sortIcon(currentSortDirection) {
return currentSortDirection === 1 ? "caret-up" : "caret-down";
}

View File

@ -1,4 +1,5 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
classNames: ["admin-report-table-row"],
options: null

View File

@ -1,21 +1,28 @@
import computed from "ember-addons/ember-computed-decorators";
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 Ember.Component.extend({
export default Component.extend({
classNameBindings: ["sortable", "twoColumns"],
classNames: ["admin-report-table"],
sortable: false,
sortDirection: 1,
perPage: Ember.computed.alias("options.perPage"),
perPage: alias("options.perPage"),
page: 0,
@computed("model.computedLabels.length")
@discourseComputed("model.computedLabels.length")
twoColumns(labelsLength) {
return labelsLength === 2;
},
@computed("totalsForSample", "options.total", "model.dates_filtering")
@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
@ -26,12 +33,16 @@ export default Ember.Component.extend({
return sum >= 1 && total && datesFiltering;
},
@computed("model.total", "options.total", "twoColumns")
@discourseComputed("model.total", "options.total", "twoColumns")
showTotal(reportTotal, total, twoColumns) {
return reportTotal && total && twoColumns;
},
@computed("model.{average,data}", "totalsForSample.1.value", "twoColumns")
@discourseComputed(
"model.{average,data}",
"totalsForSample.1.value",
"twoColumns"
)
showAverage(model, sampleTotalValue, hasTwoColumns) {
return (
model.average &&
@ -41,17 +52,17 @@ export default Ember.Component.extend({
);
},
@computed("totalsForSample.1.value", "model.data.length")
@discourseComputed("totalsForSample.1.value", "model.data.length")
averageForSample(totals, count) {
return (totals / count).toFixed(0);
},
@computed("model.data.length")
@discourseComputed("model.data.length")
showSortingUI(dataLength) {
return dataLength >= 5;
},
@computed("totalsForSampleRow", "model.computedLabels")
@discourseComputed("totalsForSampleRow", "model.computedLabels")
totalsForSample(row, labels) {
return labels.map(label => {
const computedLabel = label.compute(row);
@ -61,7 +72,7 @@ export default Ember.Component.extend({
});
},
@computed("model.data", "model.computedLabels")
@discourseComputed("model.data", "model.computedLabels")
totalsForSampleRow(rows, labels) {
if (!rows || !rows.length) return {};
@ -87,9 +98,9 @@ export default Ember.Component.extend({
return totalsRow;
},
@computed("sortLabel", "sortDirection", "model.data.[]")
@discourseComputed("sortLabel", "sortDirection", "model.data.[]")
sortedData(sortLabel, sortDirection, data) {
data = Ember.makeArray(data);
data = makeArray(data);
if (sortLabel) {
const compare = (label, direction) => {
@ -107,7 +118,7 @@ export default Ember.Component.extend({
return data;
},
@computed("sortedData.[]", "perPage", "page")
@discourseComputed("sortedData.[]", "perPage", "page")
paginatedData(data, perPage, page) {
if (perPage < data.length) {
const start = perPage * page;
@ -117,7 +128,7 @@ export default Ember.Component.extend({
return data;
},
@computed("model.data", "perPage", "page")
@discourseComputed("model.data", "perPage", "page")
pages(data, perPage, page) {
if (!data || data.length <= perPage) return [];

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: "tr"
});

View File

@ -1,9 +1,15 @@
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias, or, and, reads, equal, notEmpty } from "@ember/object/computed";
import EmberObject 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 { isNumeric } from "discourse/lib/utilities";
import { SCHEMA_VERSION, default as Report } from "admin/models/report";
import computed from "ember-addons/ember-computed-decorators";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import ENV from "discourse-common/config/environment";
const TABLE_OPTIONS = {
perPage: 8,
@ -34,7 +40,7 @@ function collapseWeekly(data, average) {
return aggregate;
}
export default Ember.Component.extend({
export default Component.extend({
classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"],
classNames: ["admin-report"],
isEnabled: true,
@ -54,13 +60,9 @@ export default Ember.Component.extend({
showHeader: true,
showTitle: true,
showFilteringUI: false,
showDatesOptions: Ember.computed.alias("model.dates_filtering"),
showExport: Ember.computed.not("model.onlyTable"),
showRefresh: Ember.computed.or(
"showDatesOptions",
"model.available_filters.length"
),
shouldDisplayTrend: Ember.computed.and("showTrend", "model.prev_period"),
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
init() {
this._super(...arguments);
@ -68,8 +70,8 @@ export default Ember.Component.extend({
this._reports = [];
},
startDate: Ember.computed.reads("filters.startDate"),
endDate: Ember.computed.reads("filters.endDate"),
startDate: reads("filters.startDate"),
endDate: reads("filters.endDate"),
didReceiveAttrs() {
this._super(...arguments);
@ -81,38 +83,34 @@ export default Ember.Component.extend({
}
},
showError: Ember.computed.or(
"showTimeoutError",
"showExceptionError",
"showNotFoundError"
),
showNotFoundError: Ember.computed.equal("model.error", "not_found"),
showTimeoutError: Ember.computed.equal("model.error", "timeout"),
showExceptionError: Ember.computed.equal("model.error", "exception"),
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
showNotFoundError: equal("model.error", "not_found"),
showTimeoutError: equal("model.error", "timeout"),
showExceptionError: equal("model.error", "exception"),
hasData: Ember.computed.notEmpty("model.data"),
hasData: notEmpty("model.data"),
@computed("dataSourceName", "model.type")
@discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-");
},
@computed("dataSourceName", "model.type")
@discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`;
},
@computed("displayedModes.length")
@discourseComputed("displayedModes.length")
showModes(displayedModesLength) {
return displayedModesLength > 1;
},
@computed("currentMode", "model.modes", "forcedModes")
@discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) {
const modes = forcedModes ? forcedModes.split(",") : reportModes;
return Ember.makeArray(modes).map(mode => {
return makeArray(modes).map(mode => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
@ -124,12 +122,12 @@ export default Ember.Component.extend({
});
},
@computed("currentMode")
@discourseComputed("currentMode")
modeComponent(currentMode) {
return `admin-report-${currentMode}`;
return `admin-report-${currentMode.replace(/_/g, "-")}`;
},
@computed("startDate")
@discourseComputed("startDate")
normalizedStartDate(startDate) {
return startDate && typeof startDate.isValid === "function"
? moment
@ -141,7 +139,7 @@ export default Ember.Component.extend({
.format("YYYYMMDD");
},
@computed("endDate")
@discourseComputed("endDate")
normalizedEndDate(endDate) {
return endDate && typeof endDate.isValid === "function"
? moment
@ -153,7 +151,7 @@ export default Ember.Component.extend({
.format("YYYYMMDD");
},
@computed(
@discourseComputed(
"dataSourceName",
"normalizedStartDate",
"normalizedEndDate",
@ -165,8 +163,8 @@ export default Ember.Component.extend({
let reportKey = "reports:";
reportKey += [
dataSourceName,
startDate.replace(/-/g, ""),
endDate.replace(/-/g, ""),
ENV.environment === "test" ? "start" : startDate.replace(/-/g, ""),
ENV.environment === "test" ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
customFilters
@ -184,6 +182,32 @@ export default Ember.Component.extend({
},
actions: {
onChangeEndDate(date) {
const startDate = moment(this.startDate);
const newEndDate = moment(date).endOf("day");
if (newEndDate.isSameOrAfter(startDate)) {
this.set("endDate", newEndDate.format("YYYY-MM-DD"));
} else {
this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD"));
}
this.send("refreshReport");
},
onChangeStartDate(date) {
const endDate = moment(this.endDate);
const newStartDate = moment(date).startOf("day");
if (newStartDate.isSameOrBefore(endDate)) {
this.set("startDate", newStartDate.format("YYYY-MM-DD"));
} else {
this.set("startDate", endDate.startOf("day").format("YYYY-MM-DD"));
}
this.send("refreshReport");
},
applyFilter(id, value) {
let customFilters = this.get("filters.customFilters") || {};
@ -286,7 +310,7 @@ export default Ember.Component.extend({
this.setProperties({ isLoading: true, rateLimitationString: null });
Ember.run.next(() => {
next(() => {
let payload = this._buildPayload(["prev_period"]);
const callback = response => {
@ -342,12 +366,12 @@ export default Ember.Component.extend({
_buildOptions(mode) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return Ember.Object.create(
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return Ember.Object.create(
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
);
}

View File

@ -1,8 +1,10 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
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 Ember.Component.extend({
@computed("theme.targets", "onlyOverridden", "showAdvanced")
export default Component.extend({
@discourseComputed("theme.targets", "onlyOverridden", "showAdvanced")
visibleTargets(targets, onlyOverridden, showAdvanced) {
return targets.filter(target => {
if (target.advanced && !showAdvanced) {
@ -15,7 +17,7 @@ export default Ember.Component.extend({
});
},
@computed("currentTargetName", "onlyOverridden", "theme.fields")
@discourseComputed("currentTargetName", "onlyOverridden", "theme.fields")
visibleFields(targetName, onlyOverridden, fields) {
fields = fields[targetName];
if (onlyOverridden) {
@ -24,14 +26,14 @@ export default Ember.Component.extend({
return fields;
},
@computed("currentTargetName", "fieldName")
@discourseComputed("currentTargetName", "fieldName")
activeSectionMode(targetName, fieldName) {
if (["settings", "translations"].includes(targetName)) return "yaml";
if (["extra_scss"].includes(targetName)) return "scss";
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@computed("fieldName", "currentTargetName", "theme")
@discourseComputed("fieldName", "currentTargetName", "theme")
activeSection: {
get(fieldName, target, model) {
return model.getField(target, fieldName);
@ -44,17 +46,21 @@ export default Ember.Component.extend({
editorId: fmt("fieldName", "currentTargetName", "%@|%@"),
@computed("maximized")
@discourseComputed("maximized")
maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand";
},
@computed("currentTargetName", "theme.targets")
@discourseComputed("currentTargetName", "theme.targets")
showAddField(currentTargetName, targets) {
return targets.find(t => t.name === currentTargetName).customNames;
},
@computed("currentTargetName", "fieldName", "theme.theme_fields.@each.error")
@discourseComputed(
"currentTargetName",
"fieldName",
"theme.theme_fields.@each.error"
)
error(target, fieldName) {
return this.theme.getError(target, fieldName);
},
@ -82,7 +88,7 @@ export default Ember.Component.extend({
toggleMaximize: function() {
this.toggleProperty("maximized");
Ember.run.next(() => this.appEvents.trigger("ace:resize"));
next(() => this.appEvents.trigger("ace:resize"));
},
onlyOverriddenChanged(value) {

View File

@ -1,16 +1,19 @@
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 } from "discourse/lib/computed";
import { i18n } from "discourse/lib/computed";
import {
default as computed,
import discourseComputed, {
observes,
on
} from "ember-addons/ember-computed-decorators";
} from "discourse-common/utils/decorators";
export default Ember.Component.extend(bufferedProperty("userField"), {
editing: Ember.computed.empty("userField.id"),
export default Component.extend(bufferedProperty("userField"), {
editing: empty("userField.id"),
classNameBindings: [":user-field"],
cantMoveUp: propertyEqual("userField", "firstField"),
@ -18,7 +21,7 @@ export default Ember.Component.extend(bufferedProperty("userField"), {
userFieldsDescription: i18n("admin.user_fields.description"),
@computed("buffered.field_type")
@discourseComputed("buffered.field_type")
bufferedFieldType(fieldType) {
return UserField.fieldTypeById(fieldType);
},
@ -27,7 +30,7 @@ export default Ember.Component.extend(bufferedProperty("userField"), {
@observes("editing")
_focusOnEdit() {
if (this.editing) {
Ember.run.scheduleOnce("afterRender", this, "_focusName");
scheduleOnce("afterRender", this, "_focusName");
}
},
@ -35,12 +38,12 @@ export default Ember.Component.extend(bufferedProperty("userField"), {
$(".user-field-name").select();
},
@computed("userField.field_type")
@discourseComputed("userField.field_type")
fieldName(fieldType) {
return UserField.fieldTypeById(fieldType).get("name");
},
@computed(
@discourseComputed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
@ -93,7 +96,7 @@ export default Ember.Component.extend(bufferedProperty("userField"), {
cancel() {
const id = this.get("userField.id");
if (Ember.isEmpty(id)) {
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();

View File

@ -1,29 +1,28 @@
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import { bufferedRender } from "discourse-common/lib/buffered-render";
import { escapeExpression } from "discourse/lib/utilities";
export default Ember.Component.extend(
bufferedRender({
classNames: ["watched-word"],
export default Component.extend({
classNames: ["watched-word"],
watchedWord: null,
xIcon: iconHTML("times").htmlSafe(),
buildBuffer(buffer) {
buffer.push(iconHTML("times"));
buffer.push(` ${escapeExpression(this.get("word.word"))}`);
},
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}`
})
);
});
}
})
);
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}`
})
);
});
}
});

View File

@ -1,25 +1,27 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["hook-event"],
typeName: Ember.computed.alias("type.name"),
typeName: alias("type.name"),
@computed("typeName")
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@computed("typeName")
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@computed("model.[]", "typeName")
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any(event => event.name === typeName);
},
@computed("eventTypeExists")
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;

View File

@ -1,15 +1,16 @@
import computed from "ember-addons/ember-computed-decorators";
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";
export default Ember.Component.extend({
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
@computed("model.status")
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) return "";
@ -20,25 +21,25 @@ export default Ember.Component.extend({
}
},
@computed("model.created_at")
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@computed("model.duration")
@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 });
},
@computed("expandDetails")
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@computed("expandDetails")
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"

View File

@ -1,32 +1,37 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import { bufferedRender } from "discourse-common/lib/buffered-render";
export default Ember.Component.extend(
bufferedRender({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["circle-o", "times-circle", "circle", "circle"],
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@computed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find(s => s.id === lastDeliveryStatus);
},
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find(s => s.id === lastDeliveryStatus);
},
@computed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@computed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
buildBuffer(buffer) {
buffer.push(iconHTML(this.icon, { class: this.class }));
buffer.push(
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
}
})
);
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")}`)
);
}
});

View File

@ -1,4 +1,5 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
didInsertElement() {
this._super(...arguments);
$("body").addClass("admin-interface");

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: ""
});

View File

@ -1,4 +1,7 @@
import { default as loadScript, loadCSS } from "discourse/lib/load-script";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
/**
An input field for a color.
@ -7,14 +10,16 @@ import { default as loadScript, loadCSS } from "discourse/lib/load-script";
@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 Ember.Component.extend({
export default Component.extend({
classNames: ["color-picker"],
@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function() {
var hex = this.hexValue;
let $text = this.$("input.hex-input");
let text = this.element.querySelector("input.hex-input");
if (this.valid) {
$text.attr(
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
@ -24,18 +29,20 @@ export default Ember.Component.extend({
);
if (this.pickerLoaded) {
this.$(".picker").spectrum({ color: "#" + this.hexValue });
$(this.element.querySelector(".picker")).spectrum({
color: "#" + this.hexValue
});
}
} else {
$text.attr("style", "");
text.setAttribute("style", "");
}
}.observes("hexValue", "brightnessValue", "valid"),
},
didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
Ember.run.schedule("afterRender", () => {
this.$(".picker")
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
@ -44,7 +51,7 @@ export default Ember.Component.extend({
});
});
});
Ember.run.schedule("afterRender", () => {
schedule("afterRender", () => {
this.hexValueChanged();
});
}

View File

@ -0,0 +1,52 @@
import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Component from "@ember/component";
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");
}
}
);
}
}
});

View File

@ -1,26 +1,31 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import computed from "ember-addons/ember-computed-decorators";
import { on, observes } from "ember-addons/ember-computed-decorators";
import { on, observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Category from "discourse/models/category";
export default Ember.Component.extend(bufferedProperty("host"), {
export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,
editing: Ember.computed.or("host.isNew", "editToggled"),
editing: or("host.isNew", "editToggled"),
@on("didInsertElement")
@observes("editing")
_focusOnInput() {
Ember.run.schedule("afterRender", () => {
this.$(".host-name").focus();
schedule("afterRender", () => {
this.element.querySelector(".host-name").focus();
});
},
@computed("buffered.host", "host.isSaving")
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || Ember.isEmpty(host);
return isSaving || isEmpty(host);
},
actions: {
@ -46,7 +51,7 @@ export default Ember.Component.extend(bufferedProperty("host"), {
host
.save(props)
.then(() => {
host.set("category", Discourse.Category.findById(this.categoryId));
host.set("category", Category.findById(this.categoryId));
this.set("editToggled", false);
})
.catch(popupAjaxError);

View File

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

View File

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

View File

@ -1,10 +1,11 @@
import { on, observes } from "ember-addons/ember-computed-decorators";
import Component from "@ember/component";
import { on, observes } from "discourse-common/utils/decorators";
import highlightSyntax from "discourse/lib/highlight-syntax";
export default Ember.Component.extend({
export default Component.extend({
@on("didInsertElement")
@observes("code")
_refresh: function() {
highlightSyntax(this.$());
highlightSyntax($(this.element));
}
});

View File

@ -1,9 +1,7 @@
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["inline-edit"],
checked: null,
@ -20,12 +18,12 @@ export default Ember.Component.extend({
this.set("checkedInternal", this.checked);
},
@computed("labelKey")
@discourseComputed("labelKey")
label(key) {
return I18n.t(key);
},
@computed("checked", "checkedInternal")
@discourseComputed("checked", "checkedInternal")
changed(checked, checkedInternal) {
return !!checked !== !!checkedInternal;
},

View File

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

View File

@ -1,12 +1,15 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import EmberObject from "@ember/object";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import AdminUser from "admin/models/admin-user";
import copyText from "discourse/lib/copy-text";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["ip-lookup"],
@computed("other_accounts.length", "totalOthersWithSameIP")
@discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
// can only delete up to 50 accounts at a time
const total = Math.min(50, totalOthersWithSameIP || 0);
@ -20,7 +23,7 @@ export default Ember.Component.extend({
if (!this.location) {
ajax("/admin/users/ip-info", { data: { ip: this.ip } }).then(location =>
this.set("location", Ember.Object.create(location))
this.set("location", EmberObject.create(location))
);
}
@ -76,7 +79,7 @@ export default Ember.Component.extend({
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
Ember.run.later(() => this.set("copied", false), 2000);
later(() => this.set("copied", false), 2000);
}
$copyRange.remove();
},

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: "tr"
});

View File

@ -1,32 +1,33 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
const ACTIONS = ["delete", "delete_replies", "edit", "none"];
export default Ember.Component.extend({
export default Component.extend({
postId: null,
postAction: null,
postEdit: null,
@computed
@discourseComputed
penaltyActions() {
return ACTIONS.map(id => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
});
},
editing: Ember.computed.equal("postAction", "edit"),
editing: equal("postAction", "edit"),
actions: {
penaltyChanged() {
let postAction = this.postAction;
// If we switch to edit mode, jump to the edit textarea
if (postAction === "edit") {
Ember.run.scheduleOnce("afterRender", () => {
let $elem = this.$();
let body = $elem.closest(".modal-body");
if (this.postAction === "edit") {
scheduleOnce("afterRender", () => {
const elem = this.element;
const body = elem.closest(".modal-body");
body.scrollTop(body.height());
$elem.find(".post-editor").focus();
elem.querySelector(".post-editor").focus();
});
}
}

View File

@ -1,14 +1,16 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import Permalink from "admin/models/permalink";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["permalink-form"],
formSubmitted: false,
permalinkType: "topic_id",
permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),
@computed
@discourseComputed
permalinkTypes() {
return [
{ id: "topic_id", name: I18n.t("admin.permalink.topic_id") },
@ -18,8 +20,23 @@ export default Ember.Component.extend({
];
},
didInsertElement() {
this._super(...arguments);
schedule("afterRender", () => {
$(this.element.querySelector(".external-url")).keydown(e => {
// enter key
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
focusPermalink() {
Ember.run.schedule("afterRender", () => this.$(".permalink-url").focus());
schedule("afterRender", () =>
this.element.querySelector(".permalink-url").focus()
);
},
actions: {
@ -60,19 +77,10 @@ export default Ember.Component.extend({
}
);
}
},
onChangePermalinkType(type) {
this.set("permalinkType", type);
}
},
didInsertElement() {
this._super(...arguments);
Ember.run.schedule("afterRender", () => {
this.$(".external-url").keydown(e => {
// enter key
if (e.keyCode === 13) {
this.send("submit");
}
});
});
}
});

View File

@ -1,5 +1,4 @@
import Category from "discourse/models/category";
import { default as computed } from "ember-addons/ember-computed-decorators";
import { readOnly } from "@ember/object/computed";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
@ -7,14 +6,11 @@ export default FilterComponent.extend({
layoutName: "admin/templates/components/report-filters/category",
@computed("filter.default")
category(categoryId) {
return Category.findById(categoryId);
},
category: readOnly("filter.default"),
actions: {
onDeselect() {
this.applyFilter(this.get("filter.id"), undefined);
onChange(categoryId) {
this.applyFilter(this.get("filter.id"), categoryId || undefined);
}
}
});

View File

@ -1,4 +1,5 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
actions: {
onChange(value) {
this.applyFilter(this.get("filter.id"), value);

View File

@ -1,19 +1,19 @@
import FilterComponent from "admin/components/report-filters/filter";
import { default as computed } from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
export default FilterComponent.extend({
classNames: ["group-filter"],
layoutName: "admin/templates/components/report-filters/group",
@computed()
@discourseComputed()
groupOptions() {
return (this.site.groups || []).map(group => {
return { name: group["name"], value: group["id"] };
});
},
@computed("filter.default")
@discourseComputed("filter.default")
groupId(filterDefault) {
return filterDefault ? parseInt(filterDefault, 10) : null;
}

View File

@ -1,9 +1,8 @@
import { schedule } from "@ember/runloop";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import { bufferedRender } from "discourse-common/lib/buffered-render";
import {
default as computed,
on
} from "ember-addons/ember-computed-decorators";
import discourseComputed, { on } from "discourse-common/utils/decorators";
/*global Resumable:true */
@ -17,114 +16,124 @@ import {
uploadText="UPLOAD"
}}
**/
export default Ember.Component.extend(
bufferedRender({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,
@on("init")
_initialize() {
this.resumable = new Resumable({
target: Discourse.getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content
}
});
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
// mark as uploading
Ember.run.later(() => this.set("isUploading", true));
});
this.resumable.on("fileProgress", file => {
// update progress
Ember.run.later(() =>
this.set("progress", parseInt(file.progress() * 100, 10))
);
});
this.resumable.on("fileSuccess", file => {
Ember.run.later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
Ember.run.later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
Ember.run.schedule("afterRender", () =>
this.resumable.assignBrowse($(this.element))
);
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
@on("init")
_initialize() {
this.resumable = new Resumable({
target: Discourse.getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content
}
},
});
@computed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
@computed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});
buildBuffer(buffer) {
const icon = this.isUploading ? "times" : "upload";
buffer.push(iconHTML(icon));
buffer.push("<span class='ru-label'>" + this.text + "</span>");
buffer.push(
"<span class='ru-progress' style='width:" + this.progress + "%'></span>"
);
},
this.resumable.on("fileProgress", file => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});
click() {
if (this.isUploading) {
this.resumable.cancel();
Ember.run.later(() => this._reset());
return false;
} else {
return true;
}
},
this.resumable.on("fileSuccess", file => {
later(() => {
// mark as not uploading anymore
this._reset();
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
})
);
},
@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},
click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},
_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},
_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
}
});

View File

@ -1,11 +1,13 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { or } from "@ember/object/computed";
import Component from "@ember/component";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["controls"],
buttonDisabled: Ember.computed.or("model.isSaving", "saveDisabled"),
buttonDisabled: or("model.isSaving", "saveDisabled"),
@computed("model.isSaving")
@discourseComputed("model.isSaving")
savingText(saving) {
return saving ? "saving" : "save";
}

View File

@ -1,3 +1,6 @@
import discourseComputed from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
/**
A form to create an IP address that will be blocked or whitelisted.
Example usage:
@ -10,20 +13,19 @@
**/
import ScreenedIpAddress from "admin/models/screened-ip-address";
import computed from "ember-addons/ember-computed-decorators";
import { on } from "ember-addons/ember-computed-decorators";
import { on } from "discourse-common/utils/decorators";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["screened-ip-address-form"],
formSubmitted: false,
actionName: "block",
@computed
@discourseComputed
adminWhitelistEnabled() {
return Discourse.SiteSettings.use_admin_ip_whitelist;
},
@computed("adminWhitelistEnabled")
@discourseComputed("adminWhitelistEnabled")
actionNames(adminWhitelistEnabled) {
if (adminWhitelistEnabled) {
return [
@ -61,8 +63,8 @@ export default Ember.Component.extend({
.then(result => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
Ember.run.schedule("afterRender", () =>
this.$(".ip-address-input").focus()
schedule("afterRender", () =>
this.element.querySelector(".ip-address-input").focus()
);
})
.catch(e => {
@ -73,7 +75,9 @@ export default Ember.Component.extend({
error: e.jqXHR.responseJSON.errors.join(". ")
})
: I18n.t("generic_error");
bootbox.alert(msg, () => this.$(".ip-address-input").focus());
bootbox.alert(msg, () =>
this.element.querySelector(".ip-address-input").focus()
);
});
}
}
@ -81,8 +85,8 @@ export default Ember.Component.extend({
@on("didInsertElement")
_init() {
Ember.run.schedule("afterRender", () => {
this.$(".ip-address-input").keydown(e => {
schedule("afterRender", () => {
$(this.element.querySelector(".ip-address-input")).keydown(e => {
if (e.keyCode === 13) {
this.send("submit");
}

View File

@ -1,6 +1,9 @@
import { on } from "ember-addons/ember-computed-decorators";
import { isEmpty } from "@ember/utils";
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
import { set } from "@ember/object";
export default Ember.Component.extend({
export default Component.extend({
classNameBindings: [":value-list", ":secret-value-list"],
inputDelimiter: null,
collection: null,
@ -42,7 +45,7 @@ export default Ember.Component.extend({
_checkInvalidInput(inputs) {
this.set("validationMessage", null);
for (let input of inputs) {
if (Ember.isEmpty(input) || input.includes("|")) {
if (isEmpty(input) || input.includes("|")) {
this.set(
"validationMessage",
I18n.t("admin.site_settings.secret_list.invalid_input")
@ -65,7 +68,7 @@ export default Ember.Component.extend({
_replaceValue(index, newValue, keyName) {
let item = this.collection[index];
Ember.set(item, keyName, newValue);
set(item, keyName, newValue);
this._saveValues();
},

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: ""
});

View File

@ -1,10 +1,15 @@
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SiteSetting from "admin/models/site-setting";
import SettingComponent from "admin/mixins/setting-component";
export default Ember.Component.extend(BufferedContent, SettingComponent, {
export default Component.extend(BufferedContent, SettingComponent, {
updateExistingUsers: null,
_save() {
const setting = this.buffered;
return SiteSetting.update(setting.get("setting"), setting.get("value"));
return SiteSetting.update(setting.get("setting"), setting.get("value"), {
updateExistingUsers: this.updateExistingUsers
});
}
});

View File

@ -1,10 +1,12 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import Component from "@ember/component";
export default Ember.Component.extend({
@computed("value")
export default Component.extend({
@discourseComputed("value")
enabled: {
get(value) {
if (Ember.isEmpty(value)) {
if (isEmpty(value)) {
return false;
}
return value.toString() === "true";

View File

@ -1,14 +1,15 @@
import computed from "ember-addons/ember-computed-decorators";
import Component from "@ember/component";
import Category from "discourse/models/category";
import { computed } from "@ember/object";
export default Ember.Component.extend({
@computed("value")
selectedCategories: {
get(value) {
return Discourse.Category.findByIds(value.split("|"));
},
set(value) {
this.set("value", value.mapBy("id").join("|"));
return value;
export default Component.extend({
selectedCategories: computed("value", function() {
return Category.findByIds(this.value.split("|").filter(Boolean));
}),
actions: {
onChangeSelectedCategories(value) {
this.set("value", (value || []).mapBy("id").join("|"));
}
}
});

View File

@ -0,0 +1,43 @@
import Component from "@ember/component";
import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers";
export default Component.extend({
tokenSeparator: "|",
createdChoices: null,
settingValue: computed("value", function() {
return this.value
.toString()
.split(this.tokenSeparator)
.filter(Boolean);
}),
settingChoices: computed(
"settingValue",
"setting.choices.[]",
"createdChoices.[]",
function() {
return [
...new Set([
...makeArray(this.settingValue),
...makeArray(this.setting.choices),
...makeArray(this.createdChoices)
])
];
}
),
actions: {
onChangeListSetting(value) {
this.set("value", value.join(this.tokenSeparator));
},
onChangeChoices(choices) {
this.set("createdChoices", [
...new Set([...makeArray(this.createdChoices), ...makeArray(choices)])
]);
}
}
});

View File

@ -1,8 +1,25 @@
import computed from "ember-addons/ember-computed-decorators";
import { computed } from "@ember/object";
import Component from "@ember/component";
export default Ember.Component.extend({
@computed()
groupChoices() {
return this.site.get("groups").map(g => g.name);
export default Component.extend({
tokenSeparator: "|",
nameProperty: "name",
valueProperty: "id",
groupChoices: computed("site.groups", function() {
return (this.site.groups || []).map(g => {
return { name: g.name, id: g.id.toString() };
});
}),
settingValue: computed("value", function() {
return (this.value || "").split(this.tokenSeparator).filter(Boolean);
}),
actions: {
onChangeGroupListSetting(value) {
this.set("value", value.join(this.tokenSeparator));
}
}
});

View File

@ -0,0 +1,15 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
@discourseComputed("value")
selectedTags: {
get(value) {
return value.split("|");
},
set(value) {
this.set("value", value.join("|"));
return value;
}
}
});

View File

@ -1,6 +1,7 @@
import Component from "@ember/component";
import showModal from "discourse/lib/show-modal";
export default Ember.Component.extend({
export default Component.extend({
actions: {
showUploadModal({ value, setting }) {
showModal("admin-uploaded-image-list", {

View File

@ -1,6 +1,7 @@
import { on } from "ember-addons/ember-computed-decorators";
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["site-text"],
classNameBindings: ["siteText.overridden"],
@ -9,11 +10,13 @@ export default Ember.Component.extend({
const term = this._searchTerm();
if (term) {
this.$(".site-text-id, .site-text-value").highlight(term, {
$(
this.element.querySelector(".site-text-id, .site-text-value")
).highlight(term, {
className: "text-highlight"
});
}
this.$(".site-text-value").ellipsis();
$(this.element.querySelector(".site-text-value")).ellipsis();
},
click() {

View File

@ -1,22 +1,27 @@
import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["table", "staff-actions"],
willDestroyElement() {
this.$().off("click.discourse-staff-logs");
$(this.element).off("click.discourse-staff-logs");
},
didInsertElement() {
this._super(...arguments);
this.$().on("click.discourse-staff-logs", "[data-link-post-id]", e => {
let postId = $(e.target).attr("data-link-post-id");
$(this.element).on(
"click.discourse-staff-logs",
"[data-link-post-id]",
e => {
let postId = $(e.target).attr("data-link-post-id");
this.store.find("post", postId).then(p => {
DiscourseURL.routeTo(p.get("url"));
});
return false;
});
this.store.find("post", postId).then(p => {
DiscourseURL.routeTo(p.get("url"));
});
return false;
}
);
}
});

View File

@ -1,3 +1,4 @@
export default Ember.Component.extend({
import Component from "@ember/component";
export default Component.extend({
tagName: ""
});

View File

@ -1,9 +1,11 @@
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import UploadMixin from "discourse/mixins/upload";
export default Ember.Component.extend(UploadMixin, {
export default Component.extend(UploadMixin, {
type: "csv",
uploadUrl: "/tags/upload",
addDisabled: Ember.computed.alias("uploading"),
addDisabled: alias("uploading"),
elementId: "tag-uploader",
validateUploadedFilesOptions() {

View File

@ -1,12 +1,20 @@
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
export default Ember.Component.extend(BufferedContent, SettingComponent, {
export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
updateUrl: url("model.id", "/admin/themes/%@/setting"),
_save() {
return this.model.saveSettings(
this.get("setting.setting"),
this.get("buffered.value")
);
return ajax(this.updateUrl, {
type: "PUT",
data: {
name: this.setting.setting,
value: this.get("buffered.value")
}
});
}
});

View File

@ -0,0 +1,26 @@
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
_save() {
return this.model
.save({ [this.setting.setting]: this.convertNamesToIds() })
.then(() => this.store.findAll("theme"));
},
convertNamesToIds() {
return this.get("buffered.value")
.split("|")
.filter(Boolean)
.map(themeName => {
if (themeName !== "") {
return this.setting.allThemes.find(theme => theme.name === themeName)
.id;
}
return themeName;
});
}
});

View File

@ -1,11 +1,13 @@
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import BufferedContent from "discourse/mixins/buffered-content";
import SettingComponent from "admin/mixins/setting-component";
export default Ember.Component.extend(BufferedContent, SettingComponent, {
export default Component.extend(BufferedContent, SettingComponent, {
layoutName: "admin/templates/components/site-setting",
setting: Ember.computed.alias("translation"),
setting: alias("translation"),
type: "string",
settingName: Ember.computed.alias("translation.key"),
settingName: alias("translation.key"),
_save() {
return this.model.saveTranslation(

View File

@ -1,17 +1,20 @@
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import { gt, and } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { escape } from "pretty-text/sanitizer";
import ENV from "discourse-common/config/environment";
const MAX_COMPONENTS = 4;
export default Ember.Component.extend({
export default Component.extend({
childrenExpanded: false,
classNames: ["themes-list-item"],
classNameBindings: ["theme.selected:selected"],
hasComponents: Ember.computed.gt("children.length", 0),
displayComponents: Ember.computed.and("hasComponents", "theme.isActive"),
displayHasMore: Ember.computed.gt("theme.childThemes.length", MAX_COMPONENTS),
hasComponents: gt("children.length", 0),
displayComponents: and("hasComponents", "theme.isActive"),
displayHasMore: gt("theme.childThemes.length", MAX_COMPONENTS),
click(e) {
if (!$(e.target).hasClass("others-count")) {
@ -30,15 +33,15 @@ export default Ember.Component.extend({
},
scheduleAnimation() {
Ember.run.schedule("afterRender", () => {
schedule("afterRender", () => {
this.animate(true);
});
},
animate(isInitial) {
const $container = this.$();
const $list = this.$(".components-list");
if ($list.length === 0 || Ember.testing) {
const $container = $(this.element);
const $list = $(this.element.querySelector(".components-list"));
if ($list.length === 0 || ENV.environment === "test") {
return;
}
const duration = 300;
@ -49,7 +52,7 @@ export default Ember.Component.extend({
}
},
@computed(
@discourseComputed(
"theme.component",
"theme.childThemes.@each.name",
"theme.childThemes.length",
@ -64,15 +67,18 @@ export default Ember.Component.extend({
children = this.childrenExpanded
? children
: children.slice(0, MAX_COMPONENTS);
return children.map(t => t.get("name"));
return children.map(t => {
const name = escape(t.name);
return t.enabled ? name : `${iconHTML("ban")} ${name}`;
});
},
@computed("children")
@discourseComputed("children")
childrenString(children) {
return children.join(", ");
},
@computed(
@discourseComputed(
"theme.childThemes.length",
"theme.component",
"childrenExpanded",

View File

@ -1,20 +1,23 @@
import { gt, equal } from "@ember/object/computed";
import Component from "@ember/component";
import { THEMES, COMPONENTS } from "admin/models/theme";
import { default as computed } from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { getOwner } from "@ember/application";
export default Ember.Component.extend({
export default Component.extend({
THEMES: THEMES,
COMPONENTS: COMPONENTS,
classNames: ["themes-list"],
hasThemes: Ember.computed.gt("themesList.length", 0),
hasActiveThemes: Ember.computed.gt("activeThemes.length", 0),
hasInactiveThemes: Ember.computed.gt("inactiveThemes.length", 0),
hasThemes: gt("themesList.length", 0),
hasActiveThemes: gt("activeThemes.length", 0),
hasInactiveThemes: gt("inactiveThemes.length", 0),
themesTabActive: Ember.computed.equal("currentTab", THEMES),
componentsTabActive: Ember.computed.equal("currentTab", COMPONENTS),
themesTabActive: equal("currentTab", THEMES),
componentsTabActive: equal("currentTab", COMPONENTS),
@computed("themes", "components", "currentTab")
@discourseComputed("themes", "components", "currentTab")
themesList(themes, components) {
if (this.themesTabActive) {
return themes;
@ -23,7 +26,7 @@ export default Ember.Component.extend({
}
},
@computed(
@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
@ -38,7 +41,7 @@ export default Ember.Component.extend({
);
},
@computed(
@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
@ -68,7 +71,7 @@ export default Ember.Component.extend({
}
},
navigateToTheme(theme) {
Ember.getOwner(this)
getOwner(this)
.lookup("router:main")
.transitionTo("adminCustomizeThemes.show", theme);
}

View File

@ -1,17 +1,18 @@
import { on } from "ember-addons/ember-computed-decorators";
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { empty, reads } from "@ember/object/computed";
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
export default Ember.Component.extend({
export default Component.extend({
classNameBindings: [":value-list"],
inputInvalid: Ember.computed.empty("newValue"),
inputInvalid: empty("newValue"),
inputDelimiter: null,
inputType: null,
newValue: "",
collection: null,
values: null,
noneKey: Ember.computed.alias("addKey"),
noneKey: reads("addKey"),
@on("didReceiveAttrs")
_setupCollection() {
@ -27,9 +28,9 @@ export default Ember.Component.extend({
);
},
@computed("choices.[]", "collection.[]")
@discourseComputed("choices.[]", "collection.[]")
filteredChoices(choices, collection) {
return Ember.makeArray(choices).filter(i => collection.indexOf(i) < 0);
return makeArray(choices).filter(i => collection.indexOf(i) < 0);
},
keyDown(event) {
@ -44,7 +45,7 @@ export default Ember.Component.extend({
addValue(newValue) {
if (this.inputInvalid) return;
this.set("newValue", "");
this.set("newValue", null);
this._addValue(newValue);
},
@ -59,12 +60,25 @@ export default Ember.Component.extend({
_addValue(value) {
this.collection.addObject(value);
if (this.choices) {
this.set("choices", this.choices.rejectBy("id", value));
} else {
this.set("choices", []);
}
this._saveValues();
},
_removeValue(value) {
const collection = this.collection;
collection.removeObject(value);
this.collection.removeObject(value);
if (this.choices) {
this.set("choices", this.choices.concat([value]).uniq());
} else {
this.set("choices", makeArray(value));
}
this._saveValues();
},

View File

@ -1,17 +1,19 @@
import { isEmpty } from "@ember/utils";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import WatchedWord from "admin/models/watched-word";
import {
default as computed,
import discourseComputed, {
on,
observes
} from "ember-addons/ember-computed-decorators";
} from "discourse-common/utils/decorators";
export default Ember.Component.extend({
export default Component.extend({
classNames: ["watched-word-form"],
formSubmitted: false,
actionKey: null,
showMessage: false,
@computed("regularExpressions")
@discourseComputed("regularExpressions")
placeholderKey(regularExpressions) {
return (
"admin.watched_words.form.placeholder" +
@ -21,12 +23,12 @@ export default Ember.Component.extend({
@observes("word")
removeMessage() {
if (this.showMessage && !Ember.isEmpty(this.word)) {
if (this.showMessage && !isEmpty(this.word)) {
this.set("showMessage", false);
}
},
@computed("word")
@discourseComputed("word")
isUniqueWord(word) {
const words = this.filteredContent || [];
const filtered = words.filter(content => content.action === this.actionKey);
@ -63,8 +65,8 @@ export default Ember.Component.extend({
message: I18n.t("admin.watched_words.form.success")
});
this.action(WatchedWord.create(result));
Ember.run.schedule("afterRender", () =>
this.$(".watched-word-input").focus()
schedule("afterRender", () =>
this.element.querySelector(".watched-word-input").focus()
);
})
.catch(e => {
@ -75,7 +77,9 @@ export default Ember.Component.extend({
error: e.jqXHR.responseJSON.errors.join(". ")
})
: I18n.t("generic_error");
bootbox.alert(msg, () => this.$(".watched-word-input").focus());
bootbox.alert(msg, () =>
this.element.querySelector(".watched-word-input").focus()
);
});
}
}
@ -83,8 +87,8 @@ export default Ember.Component.extend({
@on("didInsertElement")
_init() {
Ember.run.schedule("afterRender", () => {
this.$(".watched-word-input").keydown(e => {
schedule("afterRender", () => {
$(this.element.querySelector(".watched-word-input")).keydown(e => {
if (e.keyCode === 13) {
this.send("submit");
}

View File

@ -1,17 +1,19 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import UploadMixin from "discourse/mixins/upload";
export default Ember.Component.extend(UploadMixin, {
type: "csv",
export default Component.extend(UploadMixin, {
type: "txt",
classNames: "watched-words-uploader",
uploadUrl: "/admin/logs/watched_words/upload",
addDisabled: Ember.computed.alias("uploading"),
addDisabled: alias("uploading"),
validateUploadedFilesOptions() {
return { csvOnly: true };
return { skipValidation: true };
},
@computed("actionKey")
@discourseComputed("actionKey")
data(actionKey) {
return { action_key: actionKey };
},

View File

@ -0,0 +1,14 @@
import { popupAjaxError } from "discourse/lib/ajax-error";
import Controller from "@ember/controller";
export default Controller.extend({
actions: {
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
}
}
});

View File

@ -0,0 +1,39 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
userModes: [
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") }
],
@discourseComputed("userMode")
showUserSelector(mode) {
return mode === "single";
},
@discourseComputed("model.description", "model.username", "userMode")
saveDisabled(description, username, userMode) {
if (Ember.isBlank(description)) return true;
if (userMode === "single" && Ember.isBlank(username)) return true;
return false;
},
actions: {
changeUserMode(value) {
if (value === "all") {
this.model.set("username", null);
}
this.set("userMode", value);
},
save() {
this.model.save().catch(popupAjaxError);
},
continue() {
this.transitionToRoute("adminApiKeys.show", this.model.id);
}
}
});

View File

@ -0,0 +1,56 @@
import { bufferedProperty } from "discourse/mixins/buffered-content";
import Controller from "@ember/controller";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { empty } from "@ember/object/computed";
export default Controller.extend(bufferedProperty("model"), {
isNew: empty("model.id"),
actions: {
saveDescription() {
const buffered = this.buffered;
const attrs = buffered.getProperties("description");
this.model
.save(attrs)
.then(() => {
this.set("editingDescription", false);
this.rollbackBuffer();
})
.catch(popupAjaxError);
},
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
},
editDescription() {
this.toggleProperty("editingDescription");
if (!this.editingDescription) {
this.rollbackBuffer();
}
},
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},
deleteKey(key) {
key
.destroyRecord()
.then(() => this.transitionToRoute("adminApiKeys.index"))
.catch(popupAjaxError);
},
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
}
}
});

View File

@ -1,41 +0,0 @@
import ApiKey from "admin/models/api-key";
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
@computed("model.[]")
hasMasterKey(model) {
return !!model.findBy("user", null);
},
actions: {
generateMasterKey() {
ApiKey.generateMasterKey().then(key => this.model.pushObject(key));
},
regenerateKey(key) {
bootbox.confirm(
I18n.t("admin.api.confirm_regen"),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
key.regenerate();
}
}
);
},
revokeKey(key) {
bootbox.confirm(
I18n.t("admin.api.confirm_revoke"),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
key.revoke().then(() => this.model.removeObject(key));
}
}
);
}
}
});

View File

@ -1,15 +1,18 @@
import { alias, equal } from "@ember/object/computed";
import { inject } from "@ember/controller";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { default as computed } from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { setting, i18n } from "discourse/lib/computed";
export default Ember.Controller.extend({
adminBackups: Ember.inject.controller(),
status: Ember.computed.alias("adminBackups.model"),
export default Controller.extend({
adminBackups: inject(),
status: alias("adminBackups.model"),
uploadLabel: i18n("admin.backups.upload.label"),
backupLocation: setting("backup_location"),
localBackupStorage: Ember.computed.equal("backupLocation", "local"),
localBackupStorage: equal("backupLocation", "local"),
@computed("status.allowRestore", "status.isOperationRunning")
@discourseComputed("status.allowRestore", "status.isOperationRunning")
restoreTitle(allowRestore, isOperationRunning) {
if (!allowRestore) {
return "admin.backups.operations.restore.is_disabled";
@ -29,7 +32,7 @@ export default Ember.Controller.extend({
I18n.t("yes_value"),
confirmed => {
if (confirmed) {
Discourse.User.currentProp("hideReadOnlyAlert", true);
this.set("currentUser.hideReadOnlyAlert", true);
this._toggleReadOnlyMode(true);
}
}

View File

@ -1,6 +1,9 @@
export default Ember.Controller.extend({
adminBackups: Ember.inject.controller(),
status: Ember.computed.alias("adminBackups.model"),
import { alias } from "@ember/object/computed";
import { inject } from "@ember/controller";
import Controller from "@ember/controller";
export default Controller.extend({
adminBackups: inject(),
status: alias("adminBackups.model"),
init() {
this._super(...arguments);

View File

@ -1,9 +1,11 @@
export default Ember.Controller.extend({
noOperationIsRunning: Ember.computed.not("model.isOperationRunning"),
rollbackEnabled: Ember.computed.and(
import { not, and } from "@ember/object/computed";
import Controller from "@ember/controller";
export default Controller.extend({
noOperationIsRunning: not("model.isOperationRunning"),
rollbackEnabled: and(
"model.canRollback",
"model.restoreEnabled",
"noOperationIsRunning"
),
rollbackDisabled: Ember.computed.not("rollbackEnabled")
rollbackDisabled: not("rollbackEnabled")
});

View File

@ -0,0 +1,37 @@
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
saving: false,
replaceBadgeOwners: false,
actions: {
massAward() {
const file = document.querySelector("#massAwardCSVUpload").files[0];
if (this.model && file) {
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData()
};
options.data.append("file", file);
options.data.append("replace_badge_owners", this.replaceBadgeOwners);
this.set("saving", true);
ajax(`/admin/badges/award/${this.model.id}`, options)
.then(() => {
bootbox.alert(I18n.t("admin.badges.mass_award.success"));
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
} else {
bootbox.alert(I18n.t("admin.badges.mass_award.aborted"));
}
}
}
});

View File

@ -1,24 +1,52 @@
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Controller, { inject } from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { propertyNotEqual } from "discourse/lib/computed";
import computed from "ember-addons/ember-computed-decorators";
import { run } from "@ember/runloop";
export default Ember.Controller.extend(bufferedProperty("model"), {
adminBadges: Ember.inject.controller(),
export default Controller.extend(bufferedProperty("model"), {
adminBadges: inject(),
saving: false,
savingStatus: "",
badgeTypes: Ember.computed.alias("adminBadges.badgeTypes"),
badgeGroupings: Ember.computed.alias("adminBadges.badgeGroupings"),
badgeTriggers: Ember.computed.alias("adminBadges.badgeTriggers"),
protectedSystemFields: Ember.computed.alias(
"adminBadges.protectedSystemFields"
),
readOnly: Ember.computed.alias("buffered.system"),
badgeTypes: reads("adminBadges.badgeTypes"),
badgeGroupings: reads("adminBadges.badgeGroupings"),
badgeTriggers: reads("adminBadges.badgeTriggers"),
protectedSystemFields: reads("adminBadges.protectedSystemFields"),
readOnly: reads("buffered.system"),
showDisplayName: propertyNotEqual("name", "displayName"),
@computed("model.query", "buffered.query")
init() {
this._super(...arguments);
// this is needed because the model doesnt have default values
// and as we are using a bufferedProperty it's not accessible
// in any other way
run.next(() => {
if (this.model) {
if (!this.model.badge_type_id) {
this.model.set(
"badge_type_id",
this.get("badgeTypes.firstObject.id")
);
}
if (!this.model.badge_grouping_id) {
this.model.set(
"badge_grouping_id",
this.get("badgeGroupings.firstObject.id")
);
}
if (!this.model.trigger) {
this.model.set("trigger", this.get("badgeTriggers.firstObject.id"));
}
}
});
},
@discourseComputed("model.query", "buffered.query")
hasQuery(modelQuery, bufferedQuery) {
if (bufferedQuery) {
return bufferedQuery.trim().length > 0;
@ -26,10 +54,11 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
return modelQuery && modelQuery.trim().length > 0;
},
@observes("model.id")
_resetSaving: function() {
this.set("saving", false);
this.set("savingStatus", "");
}.observes("model.id"),
},
actions: {
save() {

View File

@ -1 +1,18 @@
export default Ember.Controller.extend();
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
routing: service("-routing"),
@discourseComputed("routing.currentRouteName")
selectedRoute() {
const currentRoute = this.routing.currentRouteName;
const indexRoute = "adminBadges.index";
if (currentRoute === indexRoute) {
return "adminBadges.show";
} else {
return this.routing.currentRouteName;
}
}
});

View File

@ -1,7 +1,9 @@
import computed from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { later } from "@ember/runloop";
import Controller from "@ember/controller";
export default Ember.Controller.extend({
@computed("model.colors", "onlyOverridden")
export default Controller.extend({
@discourseComputed("model.colors", "onlyOverridden")
colors(allColors, onlyOverridden) {
if (onlyOverridden) {
return allColors.filter(color => color.get("overridden"));
@ -40,7 +42,7 @@ export default Ember.Controller.extend({
);
}
Ember.run.later(() => {
later(() => {
this.set("model.savingStatus", null);
}, 2000);

View File

@ -1,20 +1,22 @@
import EmberObject from "@ember/object";
import Controller from "@ember/controller";
import showModal from "discourse/lib/show-modal";
import { default as computed } from "ember-addons/ember-computed-decorators";
import discourseComputed from "discourse-common/utils/decorators";
export default Ember.Controller.extend({
@computed("model.@each.id")
export default Controller.extend({
@discourseComputed("model.@each.id")
baseColorScheme() {
return this.model.findBy("is_base", true);
},
@computed("model.@each.id")
@discourseComputed("model.@each.id")
baseColorSchemes() {
return this.model.filterBy("is_base", true);
},
@computed("baseColorScheme")
@discourseComputed("baseColorScheme")
baseColors(baseColorScheme) {
const baseColorsHash = Ember.Object.create({});
const baseColorsHash = EmberObject.create({});
baseColorScheme.get("colors").forEach(color => {
baseColorsHash.set(color.get("name"), color);
});

View File

@ -0,0 +1,34 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
export default Controller.extend({
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
},
@discourseComputed("model.changed", "model.isSaving")
saveDisabled(changed, isSaving) {
return !changed || isSaving;
},
actions: {
save() {
if (!this.model.saving) {
this.set("saving", true);
this.model
.update(this.model.getProperties("html", "css"))
.catch(e => {
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("admin.customize.email_style.save_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". ")
})
: I18n.t("generic_error");
bootbox.alert(msg);
})
.finally(() => this.set("model.changed", false));
}
}
}
});

View File

@ -1,11 +1,19 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(bufferedProperty("emailTemplate"), {
export default Controller.extend(bufferedProperty("emailTemplate"), {
saved: false,
@computed("buffered")
@discourseComputed("buffered.body", "buffered.subject")
saveDisabled(body, subject) {
return (
this.emailTemplate.body === body && this.emailTemplate.subject === subject
);
},
@discourseComputed("buffered")
hasMultipleSubjects(buffered) {
if (buffered.getProperties("subject")["subject"]) {
return false;

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