diff --git a/.eslintignore b/.eslintignore index cc014c8b92f..54bd9d41efd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,5 @@ app/assets/javascripts/env.js -app/assets/javascripts/main_include.js app/assets/javascripts/main_include_admin.js -app/assets/javascripts/pagedown_custom.js app/assets/javascripts/vendor.js app/assets/javascripts/locales/i18n.js app/assets/javascripts/ember-addons/ @@ -9,13 +7,12 @@ app/assets/javascripts/discourse/lib/autosize.js.es6 lib/javascripts/locale/ lib/javascripts/messageformat.js lib/javascripts/moment.js +lib/javascripts/moment-timezone-with-data.js lib/javascripts/moment_locale/ lib/highlight_js/ +plugins/**/lib/javascripts/locale public/javascripts/ -spec/phantom_js/smoke_test.js vendor/ test/javascripts/test_helper.js -test/javascripts/test_helper.js test/javascripts/fixtures test/javascripts/helpers/assertions.js -app/assets/javascripts/ember-addons/ diff --git a/.gitattributes b/.gitattributes index ae7015b472a..546b134a0ce 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,11 +1,14 @@ # Set default behaviour, in case users don't have core.autocrlf set. * text=auto -# Explicitly declare text files we want to always be normalized and converted +# Treat email fixtures as binary files so CRLF are not converted to LF. +*.eml binary + +# Explicitly declare text files we want to always be normalized and converted # to native line endings on checkout. *.yml text -# Custom for Visual Studio, very unlikely, but lets keep it +# Custom for Visual Studio, very unlikely, but lets keep it *.cs diff=csharp *.sln merge=union *.csproj merge=union diff --git a/.gitignore b/.gitignore index 8a1f8652b9a..cf4493d4427 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,6 @@ public/tombstone/* # Ignore bundler config /.bundle -/.vagrant -/.vagrantfile /cache /coverage/* @@ -48,13 +46,13 @@ bootsnap-compile-cache/ # Ignore plugins except for the bundled ones. /plugins/* -!/plugins/emoji/ !/plugins/lazyYT/ !/plugins/poll/ !/plugins/discourse-details/ !/plugins/discourse-nginx-performance-report !/plugins/discourse-narrative-bot !/plugins/discourse-presence +!/plugins/discourse-local-dates /plugins/*/auto_generated/ /spec/fixtures/plugins/my_plugin/auto_generated @@ -80,6 +78,7 @@ discourse.sublime-workspace *~ *.swp *.swo +*.swm # don't check in multisite config config/multisite.yml @@ -95,9 +94,6 @@ config/fog_credentials.yml script/download_db script/refresh_db -# temp directory for chef (used to configure vagrant VM) -chef/tmp/* - # .procfile .procfile @@ -117,9 +113,12 @@ bundler_stubs/* vendor/bundle/* *.db -#ignore jetbrains ide file +# ignore jetbrains ide file *.iml +# vim swap +*.swn + # ignore nodejs files /node_modules /package-lock.json diff --git a/.rubocop.yml b/.rubocop.yml index b897ad3b9ec..8472c89a91f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -89,12 +89,12 @@ Layout/TrailingWhitespace: Lint/Debugger: Enabled: true -Lint/BlockAlignment: +Layout/BlockAlignment: Enabled: true # Align `end` with the matching keyword or starting expression except for # assignments, where it should be aligned with the LHS. -Lint/EndAlignment: +Layout/EndAlignment: Enabled: true EnforcedStyleAlignWith: variable diff --git a/.travis.yml b/.travis.yml index 73ae46e5b09..16ccc5722be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,15 @@ language: ruby +branches: + only: + - master + - beta + - stable + env: global: - DISCOURSE_HOSTNAME=www.example.com - - RUBY_GC_MALLOC_LIMIT=50000000 + - RUBY_GLOBAL_METHOD_CACHE_SIZE=131072 matrix: - "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0" - "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0" @@ -11,8 +17,9 @@ env: addons: chrome: stable - postgresql: 9.5 + postgresql: 9.6 apt: + update: true packages: - gifsicle - jpegoptim @@ -21,13 +28,10 @@ addons: matrix: fast_finish: true - allow_failures: - - rvm: 2.5.0 rvm: - - 2.5.0 - - 2.4.2 - - 2.3.4 + - 2.5.1 + - 2.4.4 services: - redis-server @@ -36,6 +40,7 @@ sudo: required dist: trusty cache: + apt: true yarn: true directories: - vendor/bundle @@ -49,13 +54,15 @@ 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 - export PATH=$HOME/.yarn/bin:$PATH install: - - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi" - - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi" - - bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint; fi" - - bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev; fi" + - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu > /dev/null; fi" + - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3 > /dev/null; fi" + - bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint > /dev/null; fi" + - bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev > /dev/null; fi" + - bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create db:migrate > /dev/null; fi" script: - | @@ -68,11 +75,9 @@ script: eslint --ext .es6 plugins/**/test/javascripts && \ eslint app/assets/javascripts test/javascripts else - bundle exec rake db:create db:migrate - if [ '$QUNIT_RUN' == '1' ]; then - bundle exec rake qunit:test['400000'] && \ - bundle exec rake plugin:spec + bundle exec rake qunit:test['500000'] && \ + bundle exec rake plugin:qunit else bundle exec rspec && bundle exec rake plugin:spec fi diff --git a/.tx/config b/.tx/config index 26ec9541c48..bc1bcb84c93 100644 --- a/.tx/config +++ b/.tx/config @@ -2,13 +2,13 @@ host = https://www.transifex.com lang_map = el_GR: el, es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi -[discourse-org.clientenyml] +[discourse-org.core-client-yml] file_filter = config/locales/client..yml source_file = config/locales/client.en.yml source_lang = en type = YML -[discourse-org.serverenyml] +[discourse-org.core-server-yml] file_filter = config/locales/server..yml source_file = config/locales/server.en.yml source_lang = en @@ -50,6 +50,36 @@ source_file = plugins/discourse-presence/config/locales/server.en.yml source_lang = en type = YML +[discourse-org.coreplugindetailsclientyml] +file_filter = plugins/discourse-details/config/locales/client..yml +source_file = plugins/discourse-details/config/locales/client.en.yml +source_lang = en +type = YML + +[discourse-org.coreplugindetailsserveryml] +file_filter = plugins/discourse-details/config/locales/server..yml +source_file = plugins/discourse-details/config/locales/server.en.yml +source_lang = en +type = YML + +[discourse-org.corepluginnginx-performance-reportserveryml] +file_filter = plugins/discourse-nginx-performance-report/config/locales/server..yml +source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml +source_lang = en +type = YML + +[discourse-org.core-plugin-local-dates-client-yml] +file_filter = plugins/discourse-local-dates/config/locales/client..yml +source_file = plugins/discourse-local-dates/config/locales/client.en.yml +source_lang = en +type = YML + +[discourse-org.core-plugin-local-dates-server-yml] +file_filter = plugins/discourse-local-dates/config/locales/server..yml +source_file = plugins/discourse-local-dates/config/locales/server.en.yml +source_lang = en +type = YML + [discourse-org.403html] file_filter = public/403..html source_file = public/403.html diff --git a/Brewfile b/Brewfile index ba4f913899d..89595137b60 100644 --- a/Brewfile +++ b/Brewfile @@ -1,11 +1,5 @@ # Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew) -# add this repo to Homebrew's sources -tap 'homebrew/dupes' - -# install the gcc compiler required for ruby -brew 'apple-gcc42' - # you probably already have git installed; ensure that it is the latest version brew 'git' diff --git a/Gemfile b/Gemfile index e558ffb47c5..f5a03d7d5d3 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,7 @@ source 'https://rubygems.org' # if there is a super emergency and rubygems is playing up, try #source 'http://production.cf.rubygems.org' -# does not install in linux ATM, so hack this for now -gem 'bootsnap', require: false +gem 'bootsnap', require: false, platform: :mri def rails_master? ENV["RAILS_MASTER"] == '1' @@ -25,8 +24,7 @@ else gem 'seed-fu' end -gem 'mail' -gem 'mime-types', require: 'mime/types/columnar' +gem 'mail', '2.7.1.rc1', require: false gem 'mini_mime' gem 'mini_suffix' @@ -36,12 +34,12 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.33' +gem 'onebox', '1.8.48' gem 'http_accept_language', '~>2.0.5', require: false gem 'ember-rails', '0.18.5' -gem 'ember-source' +gem 'ember-source', '2.13.3' gem 'ember-handlebars-template', '0.7.5' gem 'barber' @@ -49,18 +47,18 @@ gem 'message_bus' gem 'rails_multisite' -gem 'fast_xs' +gem 'fast_xs', platform: :mri -gem 'fast_xor' +# may move to xorcist post: https://github.com/fny/xorcist/issues/4 +gem 'fast_xor', platform: :mri -# Forked until https://github.com/sdsykes/fastimage/pull/93 is merged -gem 'discourse_fastimage', require: 'fastimage' +gem 'fastimage' gem 'aws-sdk-s3', require: false gem 'excon', require: false gem 'unf', require: false -gem 'email_reply_trimmer', '0.1.8' +gem 'email_reply_trimmer', '~> 0.1' # Forked until https://github.com/toy/image_optim/pull/149 is merged gem 'discourse_image_optim', require: 'image_optim' @@ -68,9 +66,6 @@ gem 'multi_json' gem 'mustache' gem 'nokogiri' -# this may end up deprecating nokogiri -gem 'oga', require: false - gem 'omniauth' gem 'omniauth-openid' gem 'openid-redis-store' @@ -83,7 +78,7 @@ gem 'omniauth-oauth2', require: false gem 'omniauth-google-oauth2' gem 'oj' -gem 'pg' +gem 'pg', '~> 0.21.0' gem 'pry-rails', require: false gem 'r2', '~> 0.2.5', require: false gem 'rake' @@ -120,8 +115,7 @@ group :test, :development do gem 'listen', require: false gem 'certified', require: false # later appears to break Fabricate(:topic, category: category) - gem 'fabrication', '2.9.8', require: false - gem 'discourse-qunit-rails', require: 'qunit-rails' + gem 'fabrication', require: false gem 'mocha', require: false gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false @@ -145,7 +139,7 @@ end # this is an optional gem, it provides a high performance replacement # to String#blank? a method that is called quite frequently in current # ActiveRecord, this may change in the future -gem 'fast_blank' +gem 'fast_blank', platform: :mri # this provides a very efficient lru cache gem 'lru_redux' @@ -159,7 +153,7 @@ gem 'htmlentities', require: false gem 'flamegraph', require: false gem 'rack-mini-profiler', require: false -gem 'unicorn', require: false +gem 'unicorn', require: false, platform: :mri gem 'puma', require: false gem 'rbtrace', require: false, platform: :mri gem 'gc_tracer', require: false, platform: :mri @@ -179,9 +173,17 @@ gem 'logster' gem 'sassc', require: false +gem 'rotp' +gem 'rqrcode' + +gem 'sshkey', require: false + if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' gem 'sqlite3', '~> 1.3.13' gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md' + gem 'reverse_markdown' end + +gem 'webpush', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 48b2c7df1c5..50f5b5c6935 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,9 +41,8 @@ GEM annotate (2.7.2) activerecord (>= 3.2, < 6.0) rake (>= 10.4, < 13.0) - ansi (1.5.0) arel (8.0.0) - ast (2.3.0) + ast (2.4.0) aws-partitions (1.24.0) aws-sdk-core (3.6.0) aws-partitions (~> 1.0) @@ -64,9 +63,9 @@ GEM coderay (>= 1.0.0) erubis (>= 2.6.6) rack (>= 0.9.0) - binding_of_caller (0.7.2) + binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.0.0) + bootsnap (1.1.8) msgpack (~> 1.0) builder (3.2.3) bullet (5.5.1) @@ -74,25 +73,23 @@ GEM uniform_notifier (~> 1.10.0) byebug (9.0.6) certified (1.0.0) - coderay (1.1.1) + chunky_png (1.3.8) + coderay (1.1.2) concurrent-ruby (1.0.5) connection_pool (2.2.1) cppjieba_rb (0.3.0) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.2) + crass (1.0.3) debug_inspector (0.0.3) diff-lcs (1.3) - discourse-qunit-rails (0.0.11) - railties - discourse_fastimage (2.1.0) discourse_image_optim (0.24.5) exifr (~> 1.2, >= 1.2.2) fspath (~> 3.0) image_size (~> 1.5) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - email_reply_trimmer (0.1.8) + email_reply_trimmer (0.1.12) ember-data-source (2.2.1) ember-source (>= 1.8, < 3.0) ember-handlebars-template (0.7.5) @@ -120,6 +117,7 @@ GEM rake rake-compiler fast_xs (0.8.0) + fastimage (2.1.1) ffi (1.9.18) flamegraph (0.9.5) foreman (0.84.0) @@ -133,6 +131,7 @@ GEM hashie (3.5.5) highline (1.7.8) hiredis (0.6.1) + hkdf (0.3.0) htmlentities (4.3.4) http_accept_language (2.0.5) i18n (0.8.6) @@ -144,7 +143,7 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) jwt (1.5.6) - kgio (2.11.0) + kgio (2.11.1) libv8 (6.3.292.48.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) @@ -159,21 +158,18 @@ GEM logstash-logger (0.25.1) logstash-event (~> 1.2) logster (1.2.9) - loofah (2.1.1) + loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) - mail (2.6.6) - mime-types (>= 1.16, < 4) - memory_profiler (0.9.8) - message_bus (2.1.1) + mail (2.7.1.rc1) + mini_mime (>= 0.1.1) + memory_profiler (0.9.10) + message_bus (2.1.5) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) - mini_mime (0.1.3) + mini_mime (1.0.0) mini_portile2 (2.3.0) mini_racer (0.1.15) libv8 (~> 6.3) @@ -184,14 +180,14 @@ GEM metaclass (~> 0.0.1) mock_redis (0.17.3) moneta (1.0.0) - msgpack (1.1.0) - multi_json (1.12.1) + msgpack (1.2.4) + multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) mustache (1.0.5) - nokogiri (1.8.1) + nokogiri (1.8.2) mini_portile2 (~> 2.3.0) - nokogumbo (1.4.13) + nokogumbo (1.5.0) nokogiri oauth (0.5.1) oauth2 (1.3.1) @@ -200,10 +196,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oga (2.10) - ast - ruby-ll (~> 2.1) - oj (3.1.0) + oj (3.4.0) omniauth (1.6.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) @@ -232,8 +225,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.33) - fast_blank (>= 1.0.0) + onebox (1.8.48) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -243,10 +235,10 @@ GEM openid-redis-store (0.0.2) redis ruby-openid - parallel (1.12.0) - parser (2.4.0.0) - ast (~> 2.2) - pg (0.20.0) + parallel (1.12.1) + parser (2.5.0.3) + ast (~> 2.4.0) + pg (0.21.0) powerpack (0.1.1) progress (3.3.1) pry (0.10.4) @@ -260,22 +252,22 @@ GEM public_suffix (2.0.5) puma (3.9.1) r2 (0.2.6) - rack (2.0.3) - rack-mini-profiler (0.10.7) + rack (2.0.5) + rack-mini-profiler (1.0.0) rack (>= 1.2.0) rack-openid (1.3.1) rack (>= 1.1.0) ruby-openid (>= 2.1.8) - rack-protection (2.0.0) + rack-protection (2.0.1) rack rack-test (0.7.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) - rails_multisite (1.1.2) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + rails_multisite (2.0.4) activerecord (> 4.2, < 6) railties (> 4.2, < 6) railties (5.1.4) @@ -284,24 +276,26 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.2.2) - rake + rainbow (3.0.0) raindrops (0.19.0) - rake (12.1.0) + rake (12.3.0) rake-compiler (1.0.4) rake rb-fsevent (0.9.8) rb-inotify (0.9.8) ffi (>= 0.5.0) - rbtrace (0.4.8) + rbtrace (0.4.10) ffi (>= 1.0.6) msgpack (>= 0.4.3) trollop (>= 1.16.2) - redis (3.3.5) - redis-namespace (1.5.3) - redis (~> 3.0, >= 3.0.4) + redis (4.0.1) + redis-namespace (1.6.0) + redis (>= 3.0.4) request_store (1.3.2) rinku (2.0.2) + rotp (3.3.0) + rqrcode (0.10.1) + chunky_png (~> 1.0) rspec (3.6.0) rspec-core (~> 3.6.0) rspec-expectations (~> 3.6.0) @@ -327,16 +321,13 @@ GEM rspec-support (~> 3.6.0) rspec-support (3.6.0) rtlit (0.0.5) - rubocop (0.51.0) + rubocop (0.53.0) parallel (~> 1.10) - parser (>= 2.3.3.1, < 3.0) + parser (>= 2.5) powerpack (~> 0.1) - rainbow (>= 2.2.2, < 3.0) + rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-ll (2.1.2) - ansi - ast ruby-openid (2.7.0) ruby-prof (0.16.2) ruby-progressbar (1.9.0) @@ -345,10 +336,10 @@ GEM nokogiri (>= 1.6.0) ruby_dep (1.5.0) safe_yaml (1.0.4) - sanitize (4.5.0) + sanitize (4.6.4) crass (~> 1.0.2) nokogiri (>= 1.4.4) - nokogumbo (~> 1.4.1) + nokogumbo (~> 1.4) sass (3.4.24) sassc (1.11.2) bundler @@ -363,11 +354,11 @@ GEM shoulda-context (1.2.2) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (5.0.5) + sidekiq (5.1.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (>= 3.3.4, < 5) + redis (>= 3.3.5, < 5) slop (3.6.0) sprockets (3.7.1) concurrent-ruby (~> 1.0) @@ -376,6 +367,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + sshkey (1.9.0) stackprof (0.2.10) thor (0.19.4) thread_safe (0.3.6) @@ -389,7 +381,7 @@ GEM unf_ext unf_ext (0.0.7.4) unicode-display_width (1.3.0) - unicorn (5.3.1) + unicorn (5.4.0) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.10.0) @@ -397,6 +389,9 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff + webpush (0.3.2) + hkdf (~> 0.2) + jwt PLATFORMS ruby @@ -419,20 +414,19 @@ DEPENDENCIES byebug certified cppjieba_rb - discourse-qunit-rails - discourse_fastimage discourse_image_optim - email_reply_trimmer (= 0.1.8) + email_reply_trimmer (~> 0.1) ember-handlebars-template (= 0.7.5) ember-rails (= 0.18.5) - ember-source + ember-source (= 2.13.3) excon execjs - fabrication (= 2.9.8) + fabrication fakeweb (~> 1.3.0) fast_blank fast_xor fast_xs + fastimage flamegraph foreman gc_tracer @@ -446,10 +440,9 @@ DEPENDENCIES logstash-logger logster lru_redux - mail + mail (= 2.7.1.rc1) memory_profiler message_bus - mime-types mini_mime mini_racer mini_suffix @@ -459,7 +452,6 @@ DEPENDENCIES multi_json mustache nokogiri - oga oj omniauth omniauth-facebook @@ -469,9 +461,9 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.33) + onebox (= 1.8.48) openid-redis-store - pg + pg (~> 0.21.0) pry-nav pry-rails puma @@ -487,6 +479,8 @@ DEPENDENCIES redis redis-namespace rinku + rotp + rqrcode rspec rspec-html-matchers rspec-rails @@ -500,6 +494,7 @@ DEPENDENCIES shoulda sidekiq sprockets-rails + sshkey stackprof thor tilt @@ -507,6 +502,7 @@ DEPENDENCIES unf unicorn webmock + webpush BUNDLED WITH - 1.16.0 + 1.16.1 diff --git a/README.md b/README.md index 9a4c6f7595d..6e04e399e9d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Browse [lots more notable Discourse instances](https://www.discourse.org/custome ## Development -1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/) or our [**Discourse Vagrant Developer Guide**](docs/VAGRANT.md), which includes a development environment in a virtual machine. +1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/). 2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md). @@ -57,7 +57,6 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https ## Contributing [![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse) -[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse) Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that accepts contributions from the public – including you! @@ -84,7 +83,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A ## Copyright / License -Copyright 2014 - 2017 Civilized Discourse Construction Kit, Inc. +Copyright 2014 - 2018 Civilized Discourse Construction Kit, Inc. Licensed under the GNU General Public License Version 2.0 (or later); you may not use this work except in compliance with the License. diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index bac277c0907..00000000000 --- a/Vagrantfile +++ /dev/null @@ -1,48 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : -# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md -# -Vagrant.configure("2") do |config| - config.vm.box = 'discourse-16.04' - config.vm.box_url = "https://www.dropbox.com/s/2132770g1e05c6d/discourse.box?dl=1" - - # Make this VM reachable on the host network as well, so that other - # VM's running other browsers can access our dev server. - config.vm.network :private_network, ip: "192.168.10.200" - - # Make it so that network access from the vagrant guest is able to - # use SSH private keys that are present on the host without copying - # them into the VM. - config.ssh.forward_agent = true - - config.vm.provider :virtualbox do |v| - # This setting gives the VM 1024MB of RAM instead of the default 384. - v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 1024].max] - - # Who has a single core cpu these days anyways? - cpu_count = 2 - - # Determine the available cores in host system. - # This mostly helps on linux, but it couldn't hurt on MacOSX. - if RUBY_PLATFORM =~ /linux/ - cpu_count = `nproc`.to_i - elsif RUBY_PLATFORM =~ /darwin/ - cpu_count = `sysctl -n hw.ncpu`.to_i - end - - # Assign additional cores to the guest OS. - v.customize ["modifyvm", :id, "--cpus", cpu_count] - v.customize ["modifyvm", :id, "--ioapic", "on"] - - # This setting makes it so that network access from inside the vagrant guest - # is able to resolve DNS using the hosts VPN connection. - v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] - end - - config.vm.network :forwarded_port, guest: 3000, host: 4000 - config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher - - nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/ - config.vm.synced_folder ".", "/vagrant", id: "vagrant-root" - -end diff --git a/app/assets/images/favicons/instagram.png b/app/assets/images/favicons/instagram.png new file mode 100644 index 00000000000..e51ea9c92fb Binary files /dev/null and b/app/assets/images/favicons/instagram.png differ diff --git a/app/assets/images/push-notifications/check.png b/app/assets/images/push-notifications/check.png new file mode 100644 index 00000000000..ad223d26f34 Binary files /dev/null and b/app/assets/images/push-notifications/check.png differ diff --git a/app/assets/images/push-notifications/discourse.png b/app/assets/images/push-notifications/discourse.png new file mode 100644 index 00000000000..46d9720633b Binary files /dev/null and b/app/assets/images/push-notifications/discourse.png differ diff --git a/app/assets/images/push-notifications/group_mentioned.png b/app/assets/images/push-notifications/group_mentioned.png new file mode 100644 index 00000000000..ebb5560414b Binary files /dev/null and b/app/assets/images/push-notifications/group_mentioned.png differ diff --git a/app/assets/images/push-notifications/linked.png b/app/assets/images/push-notifications/linked.png new file mode 100644 index 00000000000..5e25f2426a1 Binary files /dev/null and b/app/assets/images/push-notifications/linked.png differ diff --git a/app/assets/images/push-notifications/mentioned.png b/app/assets/images/push-notifications/mentioned.png new file mode 100644 index 00000000000..ebb5560414b Binary files /dev/null and b/app/assets/images/push-notifications/mentioned.png differ diff --git a/app/assets/images/push-notifications/posted.png b/app/assets/images/push-notifications/posted.png new file mode 100644 index 00000000000..41d02aff0eb Binary files /dev/null and b/app/assets/images/push-notifications/posted.png differ diff --git a/app/assets/images/push-notifications/private_message.png b/app/assets/images/push-notifications/private_message.png new file mode 100644 index 00000000000..8e71e69c7fe Binary files /dev/null and b/app/assets/images/push-notifications/private_message.png differ diff --git a/app/assets/images/push-notifications/quoted.png b/app/assets/images/push-notifications/quoted.png new file mode 100644 index 00000000000..01d889b468a Binary files /dev/null and b/app/assets/images/push-notifications/quoted.png differ diff --git a/app/assets/images/push-notifications/replied.png b/app/assets/images/push-notifications/replied.png new file mode 100644 index 00000000000..41d02aff0eb Binary files /dev/null and b/app/assets/images/push-notifications/replied.png differ diff --git a/app/assets/javascripts/admin/components/admin-graph.js.es6 b/app/assets/javascripts/admin/components/admin-graph.js.es6 index c99c7b186b5..724e9673742 100644 --- a/app/assets/javascripts/admin/components/admin-graph.js.es6 +++ b/app/assets/javascripts/admin/components/admin-graph.js.es6 @@ -1,4 +1,5 @@ import loadScript from 'discourse/lib/load-script'; +import { number } from 'discourse/lib/formatter'; export default Ember.Component.extend({ tagName: 'canvas', @@ -22,10 +23,16 @@ export default Ember.Component.extend({ data: data, options: { responsive: true, + tooltips: { + callbacks: { + title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL") + } + }, scales: { yAxes: [{ display: true, ticks: { + callback: (label) => number(label), suggestedMin: 0 } }] diff --git a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-counts.js.es6 index 46ab32f6091..1739a186b34 100644 --- a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-counts.js.es6 @@ -1,4 +1,5 @@ export default Ember.Component.extend({ + allTime: true, tagName: 'tr', reverseColors: Ember.computed.match('report.type', /^(time_to_first_response|topics_with_no_response)$/), classNameBindings: ['reverseColors'] diff --git a/app/assets/javascripts/discourse/components/latest-topic-list.js.es6 b/app/assets/javascripts/admin/components/cancel-link.js.es6 similarity index 55% rename from app/assets/javascripts/discourse/components/latest-topic-list.js.es6 rename to app/assets/javascripts/admin/components/cancel-link.js.es6 index 664eb94c31e..9250c1ae73b 100644 --- a/app/assets/javascripts/discourse/components/latest-topic-list.js.es6 +++ b/app/assets/javascripts/admin/components/cancel-link.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - classNames: ['latest-topic-list'] + tagName: '' }); diff --git a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 new file mode 100644 index 00000000000..8fae065f7bc --- /dev/null +++ b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 @@ -0,0 +1,19 @@ +import { ajax } from "discourse/lib/ajax"; +import AsyncReport from "admin/mixins/async-report"; + +export default Ember.Component.extend(AsyncReport, { + classNames: ["dashboard-inline-table"], + + fetchReport() { + this._super(); + + let payload = this.buildPayload(["total", "prev30Days"]); + + return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => { + return ajax(dataSource, payload) + .then(response => { + this.get("reports").pushObject(this.loadReport(response.report)); + }); + })); + } +}); diff --git a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 new file mode 100644 index 00000000000..5992e47953d --- /dev/null +++ b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 @@ -0,0 +1,164 @@ +import { ajax } from "discourse/lib/ajax"; +import AsyncReport from "admin/mixins/async-report"; +import Report from "admin/models/report"; +import { number } from 'discourse/lib/formatter'; +import loadScript from "discourse/lib/load-script"; +import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; + +function collapseWeekly(data, average) { + let aggregate = []; + let bucket, i; + let offset = data.length % 7; + for(i = offset; i < data.length; i++) { + + if (bucket && (i % 7 === offset)) { + if (average) { + bucket.y = parseFloat((bucket.y / 7.0).toFixed(2)); + } + aggregate.push(bucket); + bucket = null; + } + + bucket = bucket || { x: data[i].x, y: 0 }; + bucket.y += data[i].y; + } + return aggregate; +} + +export default Ember.Component.extend(AsyncReport, { + classNames: ["chart", "dashboard-mini-chart"], + total: 0, + + init() { + this._super(); + + this._colorsPool = ["rgb(0,136,204)", "rgb(235,83,148)"]; + }, + + didRender() { + this._super(); + registerTooltip($(this.element).find("[data-tooltip]")); + }, + + willDestroyElement() { + this._super(); + unregisterTooltip($(this.element).find("[data-tooltip]")); + }, + + pickColorAtIndex(index) { + return this._colorsPool[index] || this._colorsPool[0]; + }, + + fetchReport() { + this._super(); + + let payload = this.buildPayload(["prev_period"]); + + if (this._chart) { + this._chart.destroy(); + this._chart = null; + } + + return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => { + return ajax(dataSource, payload) + .then(response => { + this.get("reports").pushObject(this.loadReport(response.report)); + }); + })); + }, + + loadReport(report, previousReport) { + Report.fillMissingDates(report); + + if (report.data && report.data.length > 40) { + report.data = collapseWeekly(report.data, report.average); + } + + if (previousReport && previousReport.color.length) { + report.color = previousReport.color; + } else { + const dataSourceNameIndex = this.get("dataSourceNames").split(",").indexOf(report.type); + report.color = this.pickColorAtIndex(dataSourceNameIndex); + } + + return Report.create(report); + }, + + renderReport() { + this._super(); + + Ember.run.schedule("afterRender", () => { + const $chartCanvas = this.$(".chart-canvas"); + if (!$chartCanvas.length) return; + const context = $chartCanvas[0].getContext("2d"); + + const reportsForPeriod = this.get("reportsForPeriod"); + + const labels = Ember.makeArray(reportsForPeriod.get("firstObject.data")).map(d => d.x); + + const data = { + labels, + datasets: reportsForPeriod.map(report => { + return { + data: Ember.makeArray(report.data).map(d => number(d.y, { ceil: true })), + backgroundColor: "rgba(200,220,240,0.3)", + borderColor: report.color + }; + }) + }; + + if (this._chart) { + this._chart.destroy(); + this._chart = null; + } + + loadScript("/javascripts/Chart.min.js").then(() => { + if (this._chart) { + this._chart.destroy(); + } + this._chart = new window.Chart(context, this._buildChartConfig(data)); + }); + }); + }, + + _buildChartConfig(data) { + return { + type: "line", + data, + options: { + tooltips: { + callbacks: { + title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL") + } + }, + legend: { + display: false + }, + responsive: true, + maintainAspectRatio: false, + layout: { + padding: { + left: 0, + top: 0, + right: 0, + bottom: 0 + } + }, + scales: { + yAxes: [{ + display: true, + ticks: { callback: (label) => number(label, { ceil: true }) } + }], + xAxes: [{ + display: true, + gridLines: { display: false }, + type: "time", + time: { + parser: "YYYY-MM-DD" + } + }], + } + }, + }; + } +}); diff --git a/app/assets/javascripts/admin/components/dashboard-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-table.js.es6 new file mode 100644 index 00000000000..96b74e6e153 --- /dev/null +++ b/app/assets/javascripts/admin/components/dashboard-table.js.es6 @@ -0,0 +1,19 @@ +import { ajax } from "discourse/lib/ajax"; +import AsyncReport from "admin/mixins/async-report"; + +export default Ember.Component.extend(AsyncReport, { + classNames: ["dashboard-table"], + + fetchReport() { + this._super(); + + let payload = this.buildPayload(["total", "prev30Days"]); + + return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => { + return ajax(dataSource, payload) + .then(response => { + this.get("reports").pushObject(this.loadReport(response.report)); + }); + })); + } +}); diff --git a/app/assets/javascripts/admin/components/flag-user-lists.js.es6 b/app/assets/javascripts/admin/components/flag-user-lists.js.es6 new file mode 100644 index 00000000000..cd843ff7ab6 --- /dev/null +++ b/app/assets/javascripts/admin/components/flag-user-lists.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ['flag-user-lists'] +}); diff --git a/app/assets/javascripts/admin/components/flagged-post-title.js.es6 b/app/assets/javascripts/admin/components/flagged-post-title.js.es6 new file mode 100644 index 00000000000..7c1c013375b --- /dev/null +++ b/app/assets/javascripts/admin/components/flagged-post-title.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: 'h3' +}); diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index 0754952a011..8aff3d2a2d1 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -4,8 +4,6 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ adminTools: Ember.inject.service(), expanded: false, - suspended: false, - tagName: 'div', classNameBindings: [ ':flagged-post', @@ -13,17 +11,15 @@ export default Ember.Component.extend({ 'flaggedPost.deleted' ], + canAct: Ember.computed.alias('actableFilter'), + @computed('filter') - canAct(filter) { + actableFilter(filter) { return filter === 'active'; }, removeAfter(promise) { - return promise.then(() => { - this.attrs.removePost(); - }).catch(() => { - bootbox.alert(I18n.t("admin.flags.error")); - }); + return promise.then(() => this.attrs.removePost()); }, _spawnModal(name, model, modalClass) { @@ -33,7 +29,7 @@ export default Ember.Component.extend({ actions: { removeAfter(promise) { - this.removeAfter(promise); + return this.removeAfter(promise); }, disagree() { @@ -55,18 +51,6 @@ export default Ember.Component.extend({ filter: 'post', post_id: this.get('flaggedPost.id') }); - }, - - showSuspendModal() { - let post = this.get('flaggedPost'); - let user = post.get('user'); - this.get('adminTools').showSuspendModal( - user, - { - post, - successCallback: result => this.set('suspended', result.suspended) - } - ); } } }); diff --git a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 b/app/assets/javascripts/admin/components/penalty-post-action.js.es6 new file mode 100644 index 00000000000..d89c69a32d0 --- /dev/null +++ b/app/assets/javascripts/admin/components/penalty-post-action.js.es6 @@ -0,0 +1,32 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +const ACTIONS = ['delete', 'edit', 'none']; +export default Ember.Component.extend({ + postAction: null, + postEdit: null, + + @computed + penaltyActions() { + return ACTIONS.map(id => { + return { id, name: I18n.t(`admin.user.penalty_post_${id}`) }; + }); + }, + + editing: Ember.computed.equal('postAction', 'edit'), + + actions: { + penaltyChanged() { + let postAction = this.get('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'); + body.scrollTop(body.height()); + $elem.find('.post-editor').focus(); + }); + } + } + } +}); diff --git a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 b/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 index 0f8a29ba8c3..ef6e7596d11 100644 --- a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 +++ b/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 @@ -48,13 +48,17 @@ export default Ember.Component.extend({ action_name: this.get('actionName') }); screenedIpAddress.save().then(result => { - this.setProperties({ ip_address: '', formSubmitted: false }); - this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address)); - Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus()); + if (result.success) { + this.setProperties({ ip_address: '', formSubmitted: false }); + this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address)); + Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus()); + } else { + bootbox.alert(result.errors); + } }).catch(e => { this.set('formSubmitted', false); - const msg = (e.responseJSON && e.responseJSON.errors) ? - I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) : + const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ? + I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) : I18n.t("generic_error"); bootbox.alert(msg, () => this.$('.ip-address-input').focus()); }); diff --git a/app/assets/javascripts/admin/components/silence-details.js.es6 b/app/assets/javascripts/admin/components/silence-details.js.es6 new file mode 100644 index 00000000000..9250c1ae73b --- /dev/null +++ b/app/assets/javascripts/admin/components/silence-details.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: '' +}); diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index 73e8160a28c..98bdf950f65 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -1,96 +1,10 @@ import BufferedContent from 'discourse/mixins/buffered-content'; import SiteSetting from 'admin/models/site-setting'; -import { propertyNotEqual } from 'discourse/lib/computed'; -import computed from 'ember-addons/ember-computed-decorators'; -import { categoryLinkHTML } from 'discourse/helpers/category-link'; - -const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list']; - -export default Ember.Component.extend(BufferedContent, { - classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'], - content: Ember.computed.alias('setting'), - dirty: propertyNotEqual('buffered.value', 'setting.value'), - validationMessage: null, - - @computed("setting", "buffered.value") - preview(setting, value) { - // A bit hacky, but allows us to use helpers - if (setting.get('setting') === 'category_style') { - let category = this.site.get('categories.firstObject'); - if (category) { - return categoryLinkHTML(category, { - categoryStyle: value - }); - } - } - - let preview = setting.get('preview'); - if (preview) { - return new Handlebars.SafeString("
" + preview.replace(/\{\{value\}\}/g, value) + "
"); - } - }, - - @computed('componentType') - typeClass(componentType) { - return componentType.replace(/\_/g, '-'); - }, - - @computed("setting.setting") - settingName(setting) { - return setting.replace(/\_/g, ' '); - }, - - @computed("setting.type") - componentType(type) { - return CustomTypes.indexOf(type) !== -1 ? type : 'string'; - }, - - @computed("typeClass") - componentName(typeClass) { - return "site-settings/" + typeClass; - }, - - _watchEnterKey: function() { - const self = this; - this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) { - if (e.keyCode === 13) { // enter key - self._save(); - } - }); - }.on('didInsertElement'), - - _removeBindings: function() { - this.$().off("keydown.site-setting-enter"); - }.on("willDestroyElement"), +import SettingComponent from 'admin/mixins/setting-component'; +export default Ember.Component.extend(BufferedContent, SettingComponent, { _save() { - const setting = this.get('buffered'), - action = SiteSetting.update(setting.get('setting'), setting.get('value')); - action.then(() => { - this.set('validationMessage', null); - this.commitBuffer(); - }).catch((e) => { - if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - this.set('validationMessage', e.jqXHR.responseJSON.errors[0]); - } else { - this.set('validationMessage', I18n.t('generic_error')); - } - }); - }, - - actions: { - save() { - this._save(); - }, - - resetDefault() { - this.set('buffered.value', this.get('setting.default')); - this._save(); - }, - - cancel() { - this.rollbackBuffer(); - } + const setting = this.get('buffered'); + return SiteSetting.update(setting.get('setting'), setting.get('value')); } - }); diff --git a/app/assets/javascripts/admin/components/site-settings/bool.js.es6 b/app/assets/javascripts/admin/components/site-settings/bool.js.es6 index 40fcfb354b6..6be1a14e27a 100644 --- a/app/assets/javascripts/admin/components/site-settings/bool.js.es6 +++ b/app/assets/javascripts/admin/components/site-settings/bool.js.es6 @@ -6,7 +6,7 @@ export default Ember.Component.extend({ enabled: { get(value) { if (Ember.isEmpty(value)) { return false; } - return value === "true"; + return value.toString() === "true"; }, set(value) { this.set("value", value ? "true" : "false"); diff --git a/app/assets/javascripts/admin/components/staff-actions.js.es6 b/app/assets/javascripts/admin/components/staff-actions.js.es6 new file mode 100644 index 00000000000..9e742526afa --- /dev/null +++ b/app/assets/javascripts/admin/components/staff-actions.js.es6 @@ -0,0 +1,22 @@ +import DiscourseURL from 'discourse/lib/url'; + +export default Ember.Component.extend({ + classNames: ['table', 'staff-actions'], + + willDestroyElement() { + this.$().off('click.discourse-staff-logs'); + }, + + didInsertElement() { + this._super(); + + this.$().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; + }); + } +}); diff --git a/app/assets/javascripts/admin/components/suspension-details.js.es6 b/app/assets/javascripts/admin/components/suspension-details.js.es6 new file mode 100644 index 00000000000..9250c1ae73b --- /dev/null +++ b/app/assets/javascripts/admin/components/suspension-details.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: '' +}); diff --git a/app/assets/javascripts/admin/components/theme-setting.js.es6 b/app/assets/javascripts/admin/components/theme-setting.js.es6 new file mode 100644 index 00000000000..eb576e9b644 --- /dev/null +++ b/app/assets/javascripts/admin/components/theme-setting.js.es6 @@ -0,0 +1,9 @@ +import BufferedContent from 'discourse/mixins/buffered-content'; +import SettingComponent from 'admin/mixins/setting-component'; + +export default Ember.Component.extend(BufferedContent, SettingComponent, { + layoutName: 'admin/templates/components/site-setting', + _save() { + return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value')); + } +}); diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6 index 1e187bcd538..e7ead641f2e 100644 --- a/app/assets/javascripts/admin/components/watched-word-form.js.es6 +++ b/app/assets/javascripts/admin/components/watched-word-form.js.es6 @@ -5,7 +5,7 @@ export default Ember.Component.extend({ classNames: ['watched-word-form'], formSubmitted: false, actionKey: null, - showSuccessMessage: false, + showMessage: false, @computed('regularExpressions') placeholderKey(regularExpressions) { @@ -14,28 +14,40 @@ export default Ember.Component.extend({ }, @observes('word') - removeSuccessMessage() { - if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) { - this.set('showSuccessMessage', false); + removeMessage() { + if (this.get('showMessage') && !Ember.isEmpty(this.get('word'))) { + this.set('showMessage', false); } }, + @computed('word') + isUniqueWord(word) { + const words = this.get("filteredContent") || []; + const filtered = words.filter(content => content.action === this.get("actionKey")); + return filtered.every(content => content.word.toLowerCase() !== word.toLowerCase()); + }, + actions: { submit() { + if (!this.get("isUniqueWord")) { + this.setProperties({ showMessage: true, message: I18n.t('admin.watched_words.form.exists') }); + return; + } + if (!this.get('formSubmitted')) { this.set('formSubmitted', true); const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') }); watchedWord.save().then(result => { - this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true }); + this.setProperties({ word: '', formSubmitted: false, showMessage: true, message: I18n.t('admin.watched_words.form.success') }); this.sendAction('action', WatchedWord.create(result)); Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus()); }).catch(e => { this.set('formSubmitted', false); - const msg = (e.responseJSON && e.responseJSON.errors) ? - I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) : - I18n.t("generic_error"); + const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ? + I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) : + I18n.t("generic_error"); bootbox.alert(msg, () => this.$('.watched-word-input').focus()); }); } diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index fb91322edff..28ae718afab 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -5,12 +5,25 @@ export default Ember.Controller.extend({ maximized: false, section: null, + editRouteName: 'adminCustomizeThemes.edit', + targets: [ - {id: 0, name: I18n.t('admin.customize.theme.common')}, - {id: 1, name: I18n.t('admin.customize.theme.desktop')}, - {id: 2, name: I18n.t('admin.customize.theme.mobile')} + { id: 0, name: 'common' }, + { id: 1, name: 'desktop' }, + { id: 2, name: 'mobile' }, + { id: 3, name: 'settings' } ], + fieldsForTarget: function (target) { + const common = ["scss", "head_tag", "header", "after_header", "body_tag", "footer"]; + switch(target) { + case "common": return [...common, "embedded_scss"]; + case "desktop": return common; + case "mobile": return common; + case "settings": return ["yaml"]; + } + }, + @computed('onlyOverridden') showCommon() { return this.shouldShow('common'); @@ -26,6 +39,11 @@ export default Ember.Controller.extend({ return this.shouldShow('mobile'); }, + @computed('onlyOverridden', 'model.remote_theme') + showSettings() { + return false; + }, + @observes('onlyOverridden') onlyOverriddenChanged() { if (this.get('onlyOverridden')) { @@ -36,7 +54,7 @@ export default Ember.Controller.extend({ let fields = this.get('model.theme_fields'); let field = fields && fields.find(f => (f.target === target)); - this.replaceRoute('adminCustomizeThemes.edit', this.get('model.id'), target, field && field.name); + this.replaceRoute(this.get('editRouteName'), this.get('model.id'), target, field && field.name); } } }, @@ -51,27 +69,19 @@ export default Ember.Controller.extend({ currentTarget: 0, setTargetName: function(name) { - let target; - switch(name) { - case "common": target = 0; break; - case "desktop": target = 1; break; - case "mobile": target = 2; break; - } - - this.set("currentTarget", target); + const target = this.get('targets').find(t => t.name === name); + this.set("currentTarget", target && target.id); }, @computed("currentTarget") - currentTargetName(target) { - switch(parseInt(target)) { - case 0: return "common"; - case 1: return "desktop"; - case 2: return "mobile"; - } + currentTargetName(id) { + const target = this.get('targets').find(t => t.id === parseInt(id, 10)); + return target && target.name; }, @computed("fieldName") activeSectionMode(fieldName) { + if (fieldName === "yaml") return "yaml"; return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; }, @@ -96,15 +106,9 @@ export default Ember.Controller.extend({ } }, - @computed("currentTarget", "onlyOverridden") + @computed("currentTargetName", "onlyOverridden") fields(target, onlyOverridden) { - let fields = [ - "scss", "head_tag", "header", "after_header", "body_tag", "footer" - ]; - - if (parseInt(target) === 0) { - fields.push("embedded_scss"); - } + let fields = this.fieldsForTarget(target); if (onlyOverridden) { const model = this.get("model"); @@ -155,5 +159,4 @@ export default Ember.Controller.extend({ }); } } - }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 index 2873f62a9f4..16eff829470 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -2,11 +2,14 @@ import { default as computed } from 'ember-addons/ember-computed-decorators'; import { url } from 'discourse/lib/computed'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import showModal from 'discourse/lib/show-modal'; +import ThemeSettings from 'admin/models/theme-settings'; const THEME_UPLOAD_VAR = 2; export default Ember.Controller.extend({ + editRouteName: 'adminCustomizeThemes.edit', + @computed("model", "allThemes") parentThemes(model, allThemes) { let parents = allThemes.filter(theme => @@ -30,7 +33,7 @@ export default Ember.Controller.extend({ return text + ": " + localized.join(" , "); } }; - ['common','desktop','mobile'].forEach(target=> { + ['common', 'desktop', 'mobile'].forEach(target => { descriptions.push(description(target)); }); return descriptions.reject(d=>Em.isBlank(d)); @@ -77,6 +80,16 @@ export default Ember.Controller.extend({ return themes; }, + @computed("model.settings") + settings(settings) { + return settings.map(setting => ThemeSettings.create(setting)); + }, + + @computed("settings") + hasSettings(settings) { + return settings.length > 0; + }, + downloadUrl: url('model.id', '/admin/themes/%@'), actions: { @@ -131,7 +144,7 @@ export default Ember.Controller.extend({ }, editTheme() { - let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', this.get('model.id'), 'common', 'scss'); + let edit = ()=>this.transitionToRoute(this.get('editRouteName'), this.get('model.id'), 'common', 'scss'); if (this.get("model.remote_theme")) { bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => { diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 new file mode 100644 index 00000000000..2f24cf9edd0 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 @@ -0,0 +1,139 @@ +import { setting } from "discourse/lib/computed"; +import DiscourseURL from "discourse/lib/url"; +import computed from "ember-addons/ember-computed-decorators"; +import AdminDashboardNext from "admin/models/admin-dashboard-next"; +import Report from "admin/models/report"; +import VersionCheck from "admin/models/version-check"; + +const PROBLEMS_CHECK_MINUTES = 1; + +export default Ember.Controller.extend({ + queryParams: ["period"], + period: "monthly", + isLoading: false, + dashboardFetchedAt: null, + exceptionController: Ember.inject.controller("exception"), + showVersionChecks: setting("version_checks"), + diskSpace: Ember.computed.alias("model.attributes.disk_space"), + logSearchQueriesEnabled: setting("log_search_queries"), + availablePeriods: ["yearly", "quarterly", "monthly", "weekly"], + + @computed("problems.length") + foundProblems(problemsLength) { + return this.currentUser.get("admin") && (problemsLength || 0) > 0; + }, + + @computed("foundProblems") + thereWereProblems(foundProblems) { + if (!this.currentUser.get("admin")) { return false; } + + if (foundProblems) { + this.set("hadProblems", true); + return true; + } else { + return this.get("hadProblems") || false; + } + }, + + fetchDashboard() { + if (this.get("isLoading")) return; + + if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) { + this.set("isLoading", true); + + const versionChecks = this.siteSettings.version_checks; + + AdminDashboardNext.find().then(adminDashboardNextModel => { + + if (versionChecks) { + this.set("versionCheck", VersionCheck.create(adminDashboardNextModel.version_check)); + } + + this.setProperties({ + dashboardFetchedAt: new Date(), + model: adminDashboardNextModel, + reports: adminDashboardNextModel.reports.map(x => Report.create(x)) + }); + }).catch(e => { + this.get("exceptionController").set("thrown", e.jqXHR); + this.replaceRoute("exception"); + }).finally(() => { + this.set("isLoading", false); + }); + } + + if (!this.get("problemsFetchedAt") || moment().subtract(PROBLEMS_CHECK_MINUTES, "minutes").toDate() > this.get("problemsFetchedAt")) { + this.loadProblems(); + } + }, + + loadProblems() { + this.set("loadingProblems", true); + this.set("problemsFetchedAt", new Date()); + AdminDashboardNext.fetchProblems().then(d => { + this.set("problems", d.problems); + }).finally(() => { + this.set("loadingProblems", false); + }); + }, + + @computed("problemsFetchedAt") + problemsTimestamp(problemsFetchedAt) { + return moment(problemsFetchedAt).locale("en").format("LLL"); + }, + + @computed("period") + startDate(period) { + let fullDay = moment().locale("en").utc().subtract(1, "day"); + + switch (period) { + case "yearly": + return fullDay.subtract(1, "year").startOf("day"); + break; + case "quarterly": + return fullDay.subtract(3, "month").startOf("day"); + break; + case "weekly": + return fullDay.subtract(1, "week").startOf("day"); + break; + case "monthly": + return fullDay.subtract(1, "month").startOf("day"); + break; + default: + return fullDay.subtract(1, "month").startOf("day"); + } + }, + + @computed() + lastWeek() { + return moment().locale("en").utc().endOf("day").subtract(1, "week"); + }, + + @computed() + endDate() { + return moment().locale("en").utc().subtract(1, "day").endOf("day"); + }, + + @computed("updated_at") + updatedTimestamp(updatedAt) { + return moment(updatedAt).format("LLL"); + }, + + @computed("last_backup_taken_at") + backupTimestamp(lastBackupTakenAt) { + return moment(lastBackupTakenAt).format("LLL"); + }, + + actions: { + changePeriod(period) { + DiscourseURL.routeTo(this._reportsForPeriodURL(period)); + }, + refreshProblems() { + this.loadProblems(); + }, + }, + + _reportsForPeriodURL(period) { + return Discourse.getURL(`/admin?period=${period}`); + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 index d06b8dc06ea..973cd3d57c9 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 @@ -1,11 +1,8 @@ -import { setting } from 'discourse/lib/computed'; import AdminDashboard from 'admin/models/admin-dashboard'; -import VersionCheck from 'admin/models/version-check'; import Report from 'admin/models/report'; import AdminUser from 'admin/models/admin-user'; import computed from 'ember-addons/ember-computed-decorators'; -const PROBLEMS_CHECK_MINUTES = 1; const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources', 'top_referred_topics', 'updated_at']; @@ -18,34 +15,13 @@ export default Ember.Controller.extend({ loading: null, versionCheck: null, dashboardFetchedAt: null, - showVersionChecks: setting('version_checks'), - - @computed('problems.length') - foundProblems(problemsLength) { - return this.currentUser.get('admin') && (problemsLength || 0) > 0; - }, - - @computed('foundProblems') - thereWereProblems(foundProblems) { - if (!this.currentUser.get('admin')) { return false; } - - if (foundProblems) { - this.set('hadProblems', true); - return true; - } else { - return this.get('hadProblems') || false; - } - }, + exceptionController: Ember.inject.controller('exception'), fetchDashboard() { if (!this.get('dashboardFetchedAt') || moment().subtract(30, 'minutes').toDate() > this.get('dashboardFetchedAt')) { - this.set('dashboardFetchedAt', new Date()); this.set('loading', true); - const versionChecks = this.siteSettings.version_checks; AdminDashboard.find().then(d => { - if (versionChecks) { - this.set('versionCheck', VersionCheck.create(d.version_check)); - } + this.set('dashboardFetchedAt', new Date()); REPORTS.forEach(name => this.set(name, d[name].map(r => Report.create(r)))); @@ -56,29 +32,15 @@ export default Ember.Controller.extend({ } ATTRIBUTES.forEach(a => this.set(a, d[a])); + }).catch(e => { + this.get('exceptionController').set('thrown', e.jqXHR); + this.replaceRoute('exception'); + }).finally(() => { this.set('loading', false); }); } - - if (!this.get('problemsFetchedAt') || moment().subtract(PROBLEMS_CHECK_MINUTES, 'minutes').toDate() > this.get('problemsFetchedAt')) { - this.loadProblems(); - } }, - loadProblems() { - this.set('loadingProblems', true); - this.set('problemsFetchedAt', new Date()); - AdminDashboard.fetchProblems().then(d => { - this.set('problems', d.problems); - }).finally(() => { - this.set('loadingProblems', false); - }); - }, - - @computed('problemsFetchedAt') - problemsTimestamp(problemsFetchedAt) { - return moment(problemsFetchedAt).format('LLL'); - }, @computed('updated_at') updatedTimestamp(updatedAt) { @@ -86,9 +48,6 @@ export default Ember.Controller.extend({ }, actions: { - refreshProblems() { - this.loadProblems(); - }, showTrafficReport() { this.set("showTrafficReport", true); } diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 deleted file mode 100644 index cab20f908f0..00000000000 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ /dev/null @@ -1,110 +0,0 @@ -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Controller.extend({ - adminGroupsType: Ember.inject.controller(), - disableSave: false, - savingStatus: '', - - aliasLevelOptions: function() { - return [ - { name: I18n.t("groups.alias_levels.nobody"), value: 0 }, - { name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 }, - { name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 }, - { name: I18n.t("groups.alias_levels.everyone"), value: 99 } - ]; - }.property(), - - visibilityLevelOptions: function() { - return [ - { name: I18n.t("groups.visibility_levels.public"), value: 0 }, - { name: I18n.t("groups.visibility_levels.members"), value: 1 }, - { name: I18n.t("groups.visibility_levels.staff"), value: 2 }, - { name: I18n.t("groups.visibility_levels.owners"), value: 3 } - ]; - }.property(), - - trustLevelOptions: function() { - return [ - { name: I18n.t("groups.trust_levels.none"), value: 0 }, - { name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 } - ]; - }.property(), - - @computed('model.visibility_level', 'model.public_admission') - disableMembershipRequestSetting(visibility_level, publicAdmission) { - visibility_level = parseInt(visibility_level); - return (visibility_level !== 0) || publicAdmission; - }, - - @computed('model.visibility_level', 'model.allow_membership_requests') - disablePublicSetting(visibility_level, allowMembershipRequests) { - visibility_level = parseInt(visibility_level); - return (visibility_level !== 0) || allowMembershipRequests; - }, - - actions: { - removeOwner(member) { - const self = this, - message = I18n.t("admin.groups.delete_owner_confirm", { username: member.get("username"), group: this.get("model.name") }); - return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) { - if (confirm) { - self.get("model").removeOwner(member); - } - }); - }, - - addOwners() { - if (Em.isEmpty(this.get("model.ownerUsernames"))) { return; } - this.get("model").addOwners(this.get("model.ownerUsernames")).catch(popupAjaxError); - this.set("model.ownerUsernames", null); - }, - - save() { - const group = this.get('model'), - groupsController = this.get("adminGroupsType"), - groupType = groupsController.get("type"); - - this.set('disableSave', true); - this.set('savingStatus', I18n.t('saving')); - - let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.get('model').addObject(group)); - - promise.then(() => { - this.transitionToRoute("adminGroup", groupType, group.get('name')); - this.set('savingStatus', I18n.t('saved')); - }).catch(popupAjaxError) - .finally(() => this.set('disableSave', false)); - }, - - destroy() { - const group = this.get('model'), - groupsController = this.get('adminGroupsType'), - self = this; - - if (!group.get('id')) { - self.transitionToRoute('adminGroupsType.index', 'custom'); - return; - } - - this.set('disableSave', true); - - bootbox.confirm( - I18n.t("admin.groups.delete_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), - function(confirmed) { - if (confirmed) { - group.destroy().then(() => { - groupsController.get('model').removeObject(group); - self.transitionToRoute('adminGroups.index'); - }).catch(() => bootbox.alert(I18n.t("admin.groups.delete_failed"))) - .finally(() => self.set('disableSave', false)); - } else { - self.set('disableSave', false); - } - } - ); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-bulk-complete.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-bulk-complete.js.es6 deleted file mode 100644 index 780543b7241..00000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-bulk-complete.js.es6 +++ /dev/null @@ -1,4 +0,0 @@ -export default Ember.Controller.extend({ - adminGroupsBulk: Ember.inject.controller(), - bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse') -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 deleted file mode 100644 index 8f8b28f6b45..00000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -import { ajax } from 'discourse/lib/ajax'; -import computed from 'ember-addons/ember-computed-decorators'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; - -export default Ember.Controller.extend({ - users: null, - groupId: null, - saving: false, - bulkAddResponse: null, - - @computed('saving', 'users', 'groupId') - buttonDisabled(saving, users, groupId) { - return saving || !groupId || !users || !users.length; - }, - - actions: { - addToGroup() { - if (this.get('saving')) { return; } - - const users = this.get('users').split("\n") - .uniq() - .reject(x => x.length === 0); - - this.set('saving', true); - ajax('/admin/groups/bulk', { - data: { users, group_id: this.get('groupId') }, - method: 'PUT' - }).then(result => { - this.set('bulkAddResponse', result); - this.transitionToRoute('adminGroups.bulkComplete'); - }).catch(popupAjaxError).finally(() => { - this.set('saving', false); - }); - - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-type-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-type-index.js.es6 deleted file mode 100644 index ca80f093d1b..00000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-type-index.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Controller.extend({ - adminGroupsType: Ember.inject.controller(), - sortedGroups: Ember.computed.alias("adminGroupsType.sortedGroups"), - - @computed("sortedGroups") - messageKey(sortedGroups) { - return `admin.groups.${sortedGroups.length > 0 ? 'none_selected' : 'no_custom_groups'}`; - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 deleted file mode 100644 index 10d7ad01cfe..00000000000 --- a/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 +++ /dev/null @@ -1,20 +0,0 @@ -import { ajax } from 'discourse/lib/ajax'; -export default Ember.Controller.extend({ - sortedGroups: Ember.computed.sort('model', 'groupSorting'), - groupSorting: ['name'], - - refreshingAutoGroups: false, - - isAuto: Ember.computed.equal('type', 'automatic'), - - actions: { - refreshAutoGroups() { - this.set('refreshingAutoGroups', true); - ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(() => { - this.transitionToRoute("adminGroupsType", "automatic").then(() => { - this.set('refreshingAutoGroups', false); - }); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 index 83fde41c7e9..1b02d5575ac 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 @@ -4,6 +4,7 @@ import Report from 'admin/models/report'; import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend({ + classNames: ["admin-reports"], queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"], viewMode: 'graph', viewingTable: Em.computed.equal('viewMode', 'table'), diff --git a/app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 b/app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 index 98d061e0607..92045416561 100644 --- a/app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-search-logs-term.js.es6 @@ -1,7 +1,7 @@ export default Ember.Controller.extend({ loading: false, term: null, - period: "yearly", + period: "quarterly", searchType: "all", searchTypeOptions: [ diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 index df0738bed18..c2d96201bd4 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 @@ -1,16 +1,16 @@ +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Controller.extend({ categoryNameKey: null, adminSiteSettings: Ember.inject.controller(), - filteredContent: function() { - if (!this.get('categoryNameKey')) { return []; } - - const category = (this.get('adminSiteSettings.model') || []).findBy('nameKey', this.get('categoryNameKey')); - if (category) { - return category.siteSettings; - } else { - return []; - } - }.property('adminSiteSettings.model', 'categoryNameKey') + @computed("adminSiteSettings.model", "categoryNameKey") + category(categories, nameKey) { + return (categories || []).findBy("nameKey", nameKey); + }, + @computed("category") + filteredContent(category) { + return category ? category.siteSettings : []; + } }); diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 index 02a907d7af9..6469efa6ccf 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 @@ -3,7 +3,6 @@ import debounce from 'discourse/lib/debounce'; export default Ember.Controller.extend({ filter: null, onlyOverridden: false, - filtered: Ember.computed.notEmpty('filter'), filterContentNow(category) { // If we have no content, don't bother filtering anything @@ -14,9 +13,9 @@ export default Ember.Controller.extend({ filter = this.get('filter').toLowerCase(); } - if ((filter === undefined || filter.length < 1) && !this.get('onlyOverridden')) { + if ((!filter || 0 === filter.length) && !this.get('onlyOverridden')) { this.set('model', this.get('allSiteSettings')); - this.transitionToRoute("adminSiteSettings"); + this.transitionToRoute('adminSiteSettings'); return; } @@ -28,11 +27,11 @@ export default Ember.Controller.extend({ const siteSettings = settingsCategory.siteSettings.filter(item => { if (this.get('onlyOverridden') && !item.get('overridden')) return false; if (filter) { - if (item.get('setting').toLowerCase().indexOf(filter) > -1) return true; - if (item.get('setting').toLowerCase().replace(/_/g, ' ').indexOf(filter) > -1) return true; - if (item.get('description').toLowerCase().indexOf(filter) > -1) return true; - if (item.get('value').toLowerCase().indexOf(filter) > -1) return true; - return false; + const setting = item.get('setting').toLowerCase(); + return setting.includes(filter) || + setting.replace(/_/g, ' ').includes(filter) || + item.get('description').toLowerCase().includes(filter) || + (item.get('value') || '').toLowerCase().includes(filter); } else { return true; } @@ -49,15 +48,16 @@ export default Ember.Controller.extend({ }); all.siteSettings.pushObjects(matches.slice(0, 30)); - all.count = matches.length; + all.hasMore = matches.length > 30; + all.count = all.hasMore ? '30+' : matches.length; this.set('model', matchesGroupedByCategory); - this.transitionToRoute("adminSiteSettingsCategory", category || "all_results"); + this.transitionToRoute('adminSiteSettingsCategory', category || 'all_results'); }, filterContent: debounce(function() { - if (this.get("_skipBounce")) { - this.set("_skipBounce", false); + if (this.get('_skipBounce')) { + this.set('_skipBounce', false); } else { this.filterContentNow(); } diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index a2b7c94f175..346bc273c16 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -1,8 +1,10 @@ -import UserBadge from 'discourse/models/user-badge'; +import GrantBadgeController from "discourse/mixins/grant-badge-controller"; -export default Ember.Controller.extend({ +export default Ember.Controller.extend(GrantBadgeController, { adminUser: Ember.inject.controller(), user: Ember.computed.alias('adminUser.model'), + userBadges: Ember.computed.alias('model'), + allBadges: Ember.computed.alias('badges'), sortedBadges: Ember.computed.sort('model', 'badgeSortOrder'), badgeSortOrder: ['granted_at:desc'], @@ -41,36 +43,6 @@ export default Ember.Controller.extend({ return _(expanded).sortBy(group => group.granted_at).reverse().value(); }.property('model', 'model.[]', 'model.expandedBadges.[]'), - /** - Array of badges that have not been granted to this user. - - @property grantableBadges - @type {Boolean} - **/ - grantableBadges: function() { - var granted = {}; - this.get('model').forEach(function(userBadge) { - granted[userBadge.get('badge_id')] = true; - }); - - var badges = []; - this.get('badges').forEach(function(badge) { - if (badge.get('enabled') && (badge.get('multiple_grant') || !granted[badge.get('id')])) { - badges.push(badge); - } - }); - - return _.sortBy(badges, badge => badge.get('name')); - }.property('badges.[]', 'model.[]'), - - /** - Whether there are any badges that can be granted. - - @property noBadges - @type {Boolean} - **/ - noBadges: Em.computed.empty('grantableBadges'), - actions: { expandGroup: function(userBadge){ @@ -79,21 +51,21 @@ export default Ember.Controller.extend({ model.get('expandedBadges').pushObject(userBadge.badge.id); }, - grantBadge(badgeId) { - UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(userBadge => { - this.set('badgeReason', ''); - this.get('model').pushObject(userBadge); - Ember.run.next(() => { - // Update the selected badge ID after the combobox has re-rendered. - const newSelectedBadge = this.get('grantableBadges')[0]; - if (newSelectedBadge) { - this.set('selectedBadgeId', newSelectedBadge.get('id')); - } + grantBadge() { + this.grantBadge(this.get('selectedBadgeId'), this.get('user.username'), this.get('badgeReason')) + .then(() => { + this.set('badgeReason', ''); + Ember.run.next(() => { + // Update the selected badge ID after the combobox has re-rendered. + const newSelectedBadge = this.get('grantableBadges')[0]; + if (newSelectedBadge) { + this.set('selectedBadgeId', newSelectedBadge.get('id')); + } + }); + }, function() { + // Failure + bootbox.alert(I18n.t('generic_error')); }); - }, function() { - // Failure - bootbox.alert(I18n.t('generic_error')); - }); }, revokeBadge(userBadge) { diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index 266fdbd02c4..0d2ae55508e 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -19,9 +19,18 @@ export default Ember.Controller.extend(CanCheckEmails, { primaryGroupDirty: propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'), - automaticGroups: function() { - return this.get("model.automaticGroups").map((g) => g.name).join(", "); - }.property("model.automaticGroups"), + canDisableSecondFactor: Ember.computed.and( + 'model.second_factor_enabled', + 'model.can_disable_second_factor' + ), + + @computed("model.automaticGroups") + automaticGroups(automaticGroups) { + return automaticGroups.map(group => { + const name = Ember.String.htmlSafe(group.name); + return `${name}`; + }).join(", "); + }, userFields: function() { const siteUserFields = this.site.get('user_fields'), @@ -41,6 +50,36 @@ export default Ember.Controller.extend(CanCheckEmails, { return userPath(`${username}/preferences`); }, + @computed('model.can_delete_all_posts', 'model.staff', 'model.post_count') + deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) { + if (canDeleteAllPosts) { + return null; + } + + if (staff) { + return I18n.t('admin.user.delete_posts_forbidden_because_staff'); + } + if (postCount > this.siteSettings.delete_all_posts_max) { + return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: this.siteSettings.delete_all_posts_max}); + } else { + return I18n.t('admin.user.cant_delete_all_posts', {count: this.siteSettings.delete_user_max_post_age}); + } + }, + + @computed('model.canBeDeleted', 'model.staff') + deleteExplanation(canBeDeleted, staff) { + if (canBeDeleted) { + return null; + } + + if (staff) { + return I18n.t('admin.user.delete_forbidden_because_staff'); + } else { + return I18n.t('admin.user.delete_forbidden', {count: this.siteSettings.delete_user_max_post_age}); + } + }, + + actions: { impersonate() { return this.get("model").impersonate(); }, @@ -62,8 +101,36 @@ export default Ember.Controller.extend(CanCheckEmails, { silence() { return this.get("model").silence(); }, deleteAllPosts() { return this.get("model").deleteAllPosts(); }, anonymize() { return this.get('model').anonymize(); }, - destroy() { return this.get('model').destroy(); }, + disableSecondFactor() { return this.get('model').disableSecondFactor(); }, + + clearPenaltyHistory() { + let user = this.get('model'); + return ajax(`/admin/users/${user.get('id')}/penalty_history`, { + type: 'DELETE' + }).then(() => { + user.set('tl3_requirements.penalty_counts.total', 0); + }).catch(popupAjaxError); + }, + + destroy() { + const postCount = this.get('model.post_count'); + if (postCount <= 5) { + return this.get('model').destroy({ deletePosts: true }); + } else { + return this.get('model').destroy(); + } + }, + + viewActionLogs() { + this.get('adminTools').showActionLogs(this, { + target_user: this.get('model.username'), + }); + }, + + showFlagsReceived() { + this.get('adminTools').showFlagsReceived(this.get('model')); + }, showSuspendModal() { this.get('adminTools').showSuspendModal(this.get('model')); }, diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 index bd11f15fcff..9e899c4296d 100644 --- a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 @@ -10,11 +10,11 @@ export default Ember.Controller.extend({ return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName); }, - @computed('adminWatchedWords.model', 'actionNameKey') - filteredContent() { - if (!this.get('actionNameKey')) { return []; } + @computed('actionNameKey', 'adminWatchedWords.model') + filteredContent(actionNameKey) { + if (!actionNameKey) { return []; } - const a = this.findAction(this.get('actionNameKey')); + const a = this.findAction(actionNameKey); return a ? a.words : []; }, @@ -23,6 +23,12 @@ export default Ember.Controller.extend({ return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey); }, + @computed('actionNameKey', 'adminWatchedWords.model') + wordCount(actionNameKey) { + const a = this.findAction(actionNameKey); + return a ? a.words.length : 0; + }, + actions: { recordAdded(arg) { const a = this.findAction(this.get('actionNameKey')); diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 index dc4428fc723..7f9bc8fd861 100644 --- a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 @@ -46,6 +46,10 @@ export default Ember.Controller.extend({ actions: { clearFilter() { this.setProperties({ filter: '' }); + }, + + toggleMenu() { + $('.admin-detail').toggleClass('mobile-closed mobile-open'); } } diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 index 7bbdaa3de57..f47869d6468 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 @@ -3,9 +3,31 @@ import { ajax } from 'discourse/lib/ajax'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4]; + +const SCSS_VARIABLE_NAMES = [ + // common/foundation/colors.scss + "primary", "secondary", "tertiary", "quaternary", "header_background", + "header_primary", "highlight", "danger", "success", "love", + // common/foundation/math.scss + "E", "PI", "LN2", "SQRT2", + // common/foundation/variables.scss + "small-width", "medium-width", "large-width", + "google", "instagram", "facebook", "cas", "twitter", "yahoo", "github", + "base-font-size", "base-line-height", "base-font-family", + "primary-low", "primary-medium", + "secondary-low", "secondary-medium", + "tertiary-low", "quaternary-low", + "highlight-low", "highlight-medium", + "danger-low", "danger-medium", + "success-low", "love-low", +]; + export default Ember.Controller.extend(ModalFunctionality, { adminCustomizeThemesShow: Ember.inject.controller(), + uploadUrl: '/admin/themes/upload_asset', + onShow() { this.set('name', null); this.set('fileSelected', false); @@ -14,9 +36,24 @@ export default Ember.Controller.extend(ModalFunctionality, { enabled: Em.computed.and('nameValid', 'fileSelected'), disabled: Em.computed.not('enabled'), - @computed('name') - nameValid(name) { - return name && name.match(/^[a-z_][a-z0-9_-]*$/i); + @computed('name', 'adminCustomizeThemesShow.model.theme_fields') + errorMessage(name, themeFields) { + if (name) { + if (!name.match(/^[a-z_][a-z0-9_-]*$/i)) { + return I18n.t("admin.customize.theme.variable_name_error.invalid_syntax"); + } else if (SCSS_VARIABLE_NAMES.includes(name.toLowerCase())) { + return I18n.t("admin.customize.theme.variable_name_error.no_overwrite"); + } else if (themeFields.some(tf => THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && name === tf.name)) { + return I18n.t("admin.customize.theme.variable_name_error.must_be_unique"); + } + } + + return null; + }, + + @computed('errorMessage') + nameValid(errorMessage) { + return null === errorMessage; }, @observes('name') @@ -48,7 +85,7 @@ export default Ember.Controller.extend(ModalFunctionality, { options.data.append('file', file); - ajax('/admin/themes/upload_asset', options).then(result => { + ajax(this.get('uploadUrl'), options).then(result => { const upload = { upload_id: result.upload_id, name: this.get('name'), diff --git a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 index fe0416bc307..4c53ba68d87 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 @@ -1,5 +1,7 @@ import { ajax } from 'discourse/lib/ajax'; -export default Ember.Controller.extend({ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; + +export default Ember.Controller.extend(ModalFunctionality, { modelChanged: function(){ const model = this.get('model'); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-flags-received.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-flags-received.js.es6 new file mode 100644 index 00000000000..bf47fb4ae33 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-flags-received.js.es6 @@ -0,0 +1,17 @@ +export default Ember.Controller.extend({ + loadingFlags: null, + user: null, + + onShow() { + this.set('loadingFlags', true); + this.store.findAll('flagged-post', { + filter: 'without_custom', + user_id: this.get('model.id') + }).then(result => { + this.set('loadingFlags', false); + console.log(result); + this.set('flaggedPosts', result); + }); + } + +}); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 index d59d419ef53..e1076667b97 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 @@ -1,12 +1,35 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { ajax } from 'discourse/lib/ajax'; -// import computed from 'ember-addons/ember-computed-decorators'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { observes } from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend(ModalFunctionality, { local: Ember.computed.equal('selection', 'local'), remote: Ember.computed.equal('selection', 'remote'), selection: 'local', adminCustomizeThemes: Ember.inject.controller(), + loading: false, + keyGenUrl: '/admin/themes/generate_key_pair', + importUrl: '/admin/themes/import', + + checkPrivate: Ember.computed.match('uploadUrl', /^git/), + + @observes('privateChecked') + privateWasChecked() { + const checked = this.get('privateChecked'); + if (checked && !this._keyLoading) { + this._keyLoading = true; + ajax(this.get('keyGenUrl'), {method: 'POST'}) + .then(pair => { + this.set('privateKey', pair.private_key); + this.set('publicKey', pair.public_key); + }) + .catch(popupAjaxError) + .finally(()=>{ + this._keyLoading = false; + }); + } + }, actions: { importTheme() { @@ -21,14 +44,27 @@ export default Ember.Controller.extend(ModalFunctionality, { options.data = new FormData(); options.data.append('theme', $('#file-input')[0].files[0]); } else { - options.data = {remote: this.get('uploadUrl')}; + options.data = { + remote: this.get('uploadUrl') + }; + + if (this.get('privateChecked')){ + options.data.private_key = this.get('privateKey'); + } } - ajax('/admin/themes/import', options).then(result=>{ + this.set('loading', true); + ajax(this.get('importUrl'), options).then(result=>{ const theme = this.store.createRecord('theme',result.theme); this.get('adminCustomizeThemes').send('addTheme', theme); this.send('closeModal'); - }); + }) + .then(()=>{ + this.set('privateKey', null); + this.set('publicKey', null); + }) + .catch(popupAjaxError) + .finally(() => this.set('loading', false)); } } diff --git a/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 index 9f1aef916f8..ec55cbdbe88 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 @@ -1,26 +1,13 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; import computed from 'ember-addons/ember-computed-decorators'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; +import PenaltyController from 'admin/mixins/penalty-controller'; -export default Ember.Controller.extend(ModalFunctionality, { +export default Ember.Controller.extend(PenaltyController, { silenceUntil: null, - reason: null, - message: null, silencing: false, - user: null, - post: null, - successCallback: null, onShow() { - this.setProperties({ - silenceUntil: null, - reason: null, - message: null, - silencing: false, - loadingUser: true, - post: null, - successCallback: null, - }); + this.resetModal(); + this.setProperties({ silenceUntil: null, silencing: false }); }, @computed('silenceUntil', 'reason', 'silencing') @@ -33,18 +20,16 @@ export default Ember.Controller.extend(ModalFunctionality, { if (this.get('submitDisabled')) { return; } this.set('silencing', true); - this.get('user').silence({ - silenced_till: this.get('silenceUntil'), - reason: this.get('reason'), - message: this.get('message'), - post_id: this.get('post.id') - }).then(result => { - this.send('closeModal'); - let callback = this.get('successCallback'); - if (callback) { - callback(result); - } - }).catch(popupAjaxError).finally(() => this.set('silencing', false)); + this.penalize(() => { + return this.get('user').silence({ + silenced_till: this.get('silenceUntil'), + reason: this.get('reason'), + message: this.get('message'), + post_id: this.get('post.id'), + post_action: this.get('postAction'), + post_edit: this.get('postEdit') + }); + }).finally(() => this.set('silencing', false)); } } }); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 index efcd1426700..f66a1eec694 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 @@ -1,26 +1,13 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; import computed from 'ember-addons/ember-computed-decorators'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; +import PenaltyController from 'admin/mixins/penalty-controller'; -export default Ember.Controller.extend(ModalFunctionality, { +export default Ember.Controller.extend(PenaltyController, { suspendUntil: null, - reason: null, - message: null, suspending: false, - user: null, - post: null, - successCallback: null, onShow() { - this.setProperties({ - suspendUntil: null, - reason: null, - message: null, - suspending: false, - loadingUser: true, - post: null, - successCallback: null, - }); + this.resetModal(); + this.setProperties({ suspendUntil: null, suspending: false }); }, @computed('suspendUntil', 'reason', 'suspending') @@ -33,19 +20,17 @@ export default Ember.Controller.extend(ModalFunctionality, { if (this.get('submitDisabled')) { return; } this.set('suspending', true); - this.get('user').suspend({ - suspend_until: this.get('suspendUntil'), - reason: this.get('reason'), - message: this.get('message'), - post_id: this.get('post.id') - }).then(result => { - this.send('closeModal'); - let callback = this.get('successCallback'); - if (callback) { - callback(result); - } - }).catch(popupAjaxError).finally(() => this.set('suspending', false)); + + this.penalize(() => { + return this.get('user').suspend({ + suspend_until: this.get('suspendUntil'), + reason: this.get('reason'), + message: this.get('message'), + post_id: this.get('post.id'), + post_action: this.get('postAction'), + post_edit: this.get('postEdit') + }); + }).finally(() => this.set('suspending', false)); } } - }); diff --git a/app/assets/javascripts/admin/mixins/async-report.js.es6 b/app/assets/javascripts/admin/mixins/async-report.js.es6 new file mode 100644 index 00000000000..760a2059489 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/async-report.js.es6 @@ -0,0 +1,99 @@ +import computed from "ember-addons/ember-computed-decorators"; +import Report from "admin/models/report"; + +export default Ember.Mixin.create({ + classNameBindings: ["isLoading", "dataSourceNames"], + reports: null, + isLoading: false, + dataSourceNames: "", + title: null, + + init() { + this._super(); + this.set("reports", []); + }, + + @computed("dataSourceNames") + dataSources(dataSourceNames) { + return dataSourceNames.split(",").map(source => `/admin/reports/${source}`); + }, + + buildPayload(facets) { + let payload = { data: { cache: true, facets } }; + + if (this.get("startDate")) { + payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ"); + } + + if (this.get("endDate")) { + payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ"); + } + + if (this.get("limit")) { + payload.data.limit = this.get("limit"); + } + + return payload; + }, + + @computed("reports.[]", "startDate", "endDate", "dataSourceNames") + reportsForPeriod(reports, startDate, endDate, dataSourceNames) { + // on a slow network fetchReport could be called multiple times between + // T and T+x, and all the ajax responses would occur after T+(x+y) + // to avoid any inconsistencies we filter by period and make sure + // the array contains only unique values + reports = reports.uniqBy("report_key"); + + const sort = (r) => { + if (r.length > 1) { + return dataSourceNames + .split(",") + .map(name => r.findBy("type", name)); + } else { + return r; + } + }; + + if (!startDate || !endDate) { + return sort(reports); + } + + return sort(reports.filter(report => { + return report.report_key.includes(startDate.format("YYYYMMDD")) && + report.report_key.includes(endDate.format("YYYYMMDD")); + })); + }, + + didInsertElement() { + this._super(); + + this.fetchReport() + .finally(() => { + this.renderReport(); + }); + }, + + didUpdateAttrs() { + this._super(); + + this.fetchReport() + .finally(() => { + this.renderReport(); + }); + }, + + renderReport() { + if (!this.element || this.isDestroying || this.isDestroyed) return; + this.set("title", this.get("reportsForPeriod").map(r => r.title).join(", ")); + this.set("isLoading", false); + }, + + loadReport(jsonReport) { + return Report.create(jsonReport); + }, + + fetchReport() { + this.set("reports", []); + this.set("isLoading", true); + }, +}); diff --git a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 new file mode 100644 index 00000000000..8b5bbb06297 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 @@ -0,0 +1,41 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Mixin.create(ModalFunctionality, { + reason: null, + message: null, + postEdit: null, + postAction: null, + user: null, + post: null, + successCallback: null, + + resetModal() { + this.setProperties({ + reason: null, + message: null, + loadingUser: true, + post: null, + postEdit: null, + postAction: 'delete', + before: null, + successCallback: null + }); + }, + + penalize(cb) { + let before = this.get('before'); + let promise = before ? before() : Ember.RSVP.resolve(); + + return promise + .then(() => cb()) + .then(result => { + this.send('closeModal'); + let callback = this.get('successCallback'); + if (callback) { + callback(result); + } + }) + .catch(popupAjaxError); + } +}); diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 new file mode 100644 index 00000000000..71fc679f8d0 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -0,0 +1,107 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import { categoryLinkHTML } from 'discourse/helpers/category-link'; + +const CUSTOM_TYPES = [ + 'bool', + 'enum', + 'list', + 'url_list', + 'host_list', + 'category_list', + 'value_list', + 'category' +]; + +export default Ember.Mixin.create({ + classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'], + content: Ember.computed.alias('setting'), + validationMessage: null, + + @computed("buffered.value", "setting.value") + dirty(bufferVal, settingVal) { + if (bufferVal === null || bufferVal === undefined) bufferVal = ''; + if (settingVal === null || settingVal === undefined) settingVal = ''; + + return bufferVal.toString() !== settingVal.toString(); + }, + + @computed("setting", "buffered.value") + preview(setting, value) { + // A bit hacky, but allows us to use helpers + if (setting.get('setting') === 'category_style') { + let category = this.site.get('categories.firstObject'); + if (category) { + return categoryLinkHTML(category, { + categoryStyle: value + }); + } + } + + let preview = setting.get('preview'); + if (preview) { + return new Handlebars.SafeString("
" + preview.replace(/\{\{value\}\}/g, value) + "
"); + } + }, + + @computed('componentType') + typeClass(componentType) { + return componentType.replace(/\_/g, '-'); + }, + + @computed("setting.setting") + settingName(setting) { + return setting.replace(/\_/g, ' '); + }, + + @computed("setting.type") + componentType(type) { + return CUSTOM_TYPES.indexOf(type) !== -1 ? type : 'string'; + }, + + @computed("typeClass") + componentName(typeClass) { + return "site-settings/" + typeClass; + }, + + _watchEnterKey: function() { + const self = this; + this.$().on("keydown.setting-enter", ".input-setting-string", function (e) { + if (e.keyCode === 13) { // enter key + self._save(); + } + }); + }.on('didInsertElement'), + + _removeBindings: function() { + this.$().off("keydown.setting-enter"); + }.on("willDestroyElement"), + + _save() { + Em.warn("You should define a `_save` method", { id: "admin.mixins.setting-component" }); + return Ember.RSVP.resolve(); + }, + + actions: { + save() { + this._save().then(() => { + this.set('validationMessage', null); + this.commitBuffer(); + }).catch(e => { + if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { + this.set('validationMessage', e.jqXHR.responseJSON.errors[0]); + } else { + this.set('validationMessage', I18n.t('generic_error')); + } + }); + }, + + resetDefault() { + this.set('buffered.value', this.get('setting.default')); + this._save(); + }, + + cancel() { + this.rollbackBuffer(); + } + } +}); diff --git a/app/assets/javascripts/admin/mixins/setting-object.js.es6 b/app/assets/javascripts/admin/mixins/setting-object.js.es6 new file mode 100644 index 00000000000..0b16ed50095 --- /dev/null +++ b/app/assets/javascripts/admin/mixins/setting-object.js.es6 @@ -0,0 +1,29 @@ +export default Ember.Mixin.create({ + overridden: function() { + let val = this.get('value'), + defaultVal = this.get('default'); + + if (val === null) val = ''; + if (defaultVal === null) defaultVal = ''; + + return val.toString() !== defaultVal.toString(); + }.property('value', 'default'), + + validValues: function() { + const vals = [], + translateNames = this.get('translate_names'); + + this.get('valid_values').forEach(v => { + if (v.name && v.name.length > 0 && translateNames) { + vals.addObject({ name: I18n.t(v.name), value: v.value }); + } else { + vals.addObject(v); + } + }); + return vals; + }.property('valid_values'), + + allowsNone: function() { + if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.settings.none'; + }.property('valid_values') +}); diff --git a/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 new file mode 100644 index 00000000000..db3adf22481 --- /dev/null +++ b/app/assets/javascripts/admin/models/admin-dashboard-next.js.es6 @@ -0,0 +1,54 @@ +import { ajax } from "discourse/lib/ajax"; + +const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at" ]; + +const AdminDashboardNext = Discourse.Model.extend({}); + +AdminDashboardNext.reopenClass({ + /** + Fetch all dashboard data. This can be an expensive request when the cached data + has expired and the server must collect the data again. + + @method find + @return {jqXHR} a jQuery Promise object + **/ + find() { + + return ajax("/admin/dashboard-next.json").then(function(json) { + + var model = AdminDashboardNext.create(); + + model.set("reports", json.reports); + model.set("version_check", json.version_check); + + const attributes = {}; + ATTRIBUTES.forEach(a => attributes[a] = json[a]); + model.set("attributes", attributes); + + model.set("loaded", true); + + return model; + }); + }, + + + /** + Only fetch the list of problems that should be rendered on the dashboard. + The model will only have its "problems" attribute set. + + @method fetchProblems + @return {jqXHR} a jQuery Promise object + **/ + fetchProblems: function() { + return ajax("/admin/dashboard/problems.json", { + type: 'GET', + dataType: 'json' + }).then(function(json) { + var model = AdminDashboardNext.create(json); + model.set('loaded', true); + return model; + }); + } +}); + +export default AdminDashboardNext; diff --git a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 index ac44a7677f1..49ed8e1a078 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 @@ -19,23 +19,6 @@ AdminDashboard.reopenClass({ }); }, - /** - Only fetch the list of problems that should be rendered on the dashboard. - The model will only have its "problems" attribute set. - - @method fetchProblems - @return {jqXHR} a jQuery Promise object - **/ - fetchProblems: function() { - return ajax("/admin/dashboard/problems.json", { - type: 'GET', - dataType: 'json' - }).then(function(json) { - var model = AdminDashboard.create(json); - model.set('loaded', true); - return model; - }); - } }); export default AdminDashboard; diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index e3f1530badf..ba7528258d7 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -5,7 +5,6 @@ import { propertyNotEqual } from 'discourse/lib/computed'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import ApiKey from 'admin/models/api-key'; import Group from 'discourse/models/group'; -import TL3Requirements from 'admin/models/tl3-requirements'; import { userPath } from 'discourse/lib/url'; const wrapAdmin = user => user ? AdminUser.create(user) : null; @@ -88,21 +87,6 @@ const AdminUser = Discourse.User.extend({ }).then(() => this.set('api_key', null)); }, - deleteAllPostsExplanation: function() { - if (!this.get('can_delete_all_posts')) { - if (this.get('deleteForbidden') && this.get('staff')) { - return I18n.t('admin.user.delete_posts_forbidden_because_staff'); - } - if (this.get('post_count') > Discourse.SiteSettings.delete_all_posts_max) { - return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: Discourse.SiteSettings.delete_all_posts_max}); - } else { - return I18n.t('admin.user.cant_delete_all_posts', {count: Discourse.SiteSettings.delete_user_max_post_age}); - } - } else { - return null; - } - }.property('can_delete_all_posts', 'deleteForbidden'), - deleteAllPosts() { const user = this, message = I18n.messageFormat('admin.user.delete_all_posts_confirm_MF', { "POSTS": user.get('post_count'), "TOPICS": user.get('topic_count') }), @@ -168,6 +152,14 @@ const AdminUser = Discourse.User.extend({ }).catch(popupAjaxError); }, + disableSecondFactor() { + return ajax(`/admin/users/${this.get('id')}/disable_second_factor`, { + type: 'PUT' + }).then(() => { + this.set('second_factor_enabled', false); + }).catch(popupAjaxError); + }, + refreshBrowsers() { return ajax("/admin/users/" + this.get('id') + "/refresh_browsers", { type: 'POST' @@ -233,8 +225,6 @@ const AdminUser = Discourse.User.extend({ return this.get('trust_level') < 4; }.property('trust_level'), - isSuspended: Em.computed.equal('suspended', true), - isSilenced: Ember.computed.equal('silenced', true), canSuspend: Em.computed.not('staff'), suspendDuration: function() { @@ -293,7 +283,8 @@ const AdminUser = Discourse.User.extend({ deactivate() { return ajax('/admin/users/' + this.id + '/deactivate', { - type: 'PUT' + type: 'PUT', + data: { context: document.location.pathname } }).then(function() { window.location.reload(); }).catch(function(e) { @@ -345,8 +336,6 @@ const AdminUser = Discourse.User.extend({ }).catch(popupAjaxError); }, - anonymizeForbidden: Em.computed.not("can_be_anonymized"), - anonymize() { const user = this, message = I18n.t("admin.user.anonymize_confirm"); @@ -385,26 +374,13 @@ const AdminUser = Discourse.User.extend({ bootbox.dialog(message, buttons, { "classes": "delete-user-modal" }); }, - deleteForbidden: Em.computed.not("canBeDeleted"), - - deleteExplanation: function() { - if (this.get('deleteForbidden')) { - if (this.get('staff')) { - return I18n.t('admin.user.delete_forbidden_because_staff'); - } else { - return I18n.t('admin.user.delete_forbidden', {count: Discourse.SiteSettings.delete_user_max_post_age}); - } - } else { - return null; - } - }.property('deleteForbidden'), - destroy(opts) { const user = this, message = I18n.t("admin.user.delete_confirm"), location = document.location.pathname; const performDestroy = function(block) { + bootbox.dialog(I18n.t('admin.user.deleting_user')); let formData = { context: location }; if (block) { formData["block_email"] = true; @@ -464,11 +440,12 @@ const AdminUser = Discourse.User.extend({ }); }, - tl3Requirements: function() { - if (this.get('tl3_requirements')) { - return TL3Requirements.create(this.get('tl3_requirements')); + @computed('tl3_requirements') + tl3Requirements(requirements) { + if (requirements) { + return this.store.createRecord('tl3Requirements', requirements); } - }.property('tl3_requirements'), + }, @computed('suspended_by') suspendedBy: wrapAdmin, diff --git a/app/assets/javascripts/admin/models/flagged-post.js.es6 b/app/assets/javascripts/admin/models/flagged-post.js.es6 index acd3a0e0dfe..a1cd66ccd99 100644 --- a/app/assets/javascripts/admin/models/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/models/flagged-post.js.es6 @@ -22,6 +22,11 @@ export default Post.extend({ }); }, + @computed('post_actions') + hasDisposedBy() { + return this.get('post_actions').some(action => action.disposed_by); + }, + @computed('post_actions.@each.name_key') flaggedForSpam() { return this.get('post_actions').every(action => action.name_key === 'spam'); @@ -57,11 +62,74 @@ export default Post.extend({ }, deferFlags(deletePost) { - return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }).catch(popupAjaxError); + const action = () => { + return ajax('/admin/flags/defer/' + this.id, { + type: 'POST', cache: false, data: { delete_post: deletePost } + }); + }; + + if (deletePost && this._hasDeletableReplies()) { + return this._actOnFlagAndDeleteReplies(action); + } else { + return action().catch(popupAjaxError); + } }, agreeFlags(actionOnPost) { - return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }).catch(popupAjaxError); + const action = () => { + return ajax('/admin/flags/agree/' + this.id, { + type: 'POST', cache: false, data: { action_on_post: actionOnPost } + }); + }; + + if (actionOnPost === 'delete' && this._hasDeletableReplies()) { + return this._actOnFlagAndDeleteReplies(action); + } else { + return action().catch(popupAjaxError); + } + }, + + _hasDeletableReplies() { + return this.get('post_number') > 1 && this.get('reply_count') > 0; + }, + + _actOnFlagAndDeleteReplies(action) { + return new Ember.RSVP.Promise((resolve, reject) => { + return ajax(`/posts/${this.id}/reply-ids/all.json`).then(replies => { + const buttons = []; + + buttons.push({ + label: I18n.t('no_value'), + callback() { + action() + .then(resolve) + .catch(error => { + popupAjaxError(error); + reject(); + }); + } + }); + + buttons.push({ + label: I18n.t('yes_value'), + class: "btn-danger", + callback() { + Post.deleteMany(replies.map(r => r.id)) + .then(action) + .then(resolve) + .catch(error => { + popupAjaxError(error); + reject(); + }); + } + }); + + bootbox.dialog(I18n.t("admin.flags.delete_replies", { count: replies.length }), buttons); + }).catch(error => { + popupAjaxError(error); + reject(); + }); + }); }, postHidden: Ember.computed.alias('hidden'), diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 03e3888f97b..80c160a8d90 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,13 +1,24 @@ -import { ajax } from 'discourse/lib/ajax'; +import { ajax } from "discourse/lib/ajax"; import round from "discourse/lib/round"; -import { fmt } from 'discourse/lib/computed'; +import { fillMissingDates } from "discourse/lib/utilities"; +import computed from "ember-addons/ember-computed-decorators"; +import { number } from 'discourse/lib/formatter'; const Report = Discourse.Model.extend({ - reportUrl: fmt("type", "/admin/reports/%@"), + average: false, + percent: false, + higher_is_better: true, + + @computed("type", "start_date", "end_date") + reportUrl(type, start_date, end_date) { + start_date = moment(start_date).locale("en").format("YYYY-MM-DD"); + end_date = moment(end_date).locale("en").format("YYYY-MM-DD"); + return Discourse.getURL(`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`); + }, valueAt(numDaysAgo) { if (this.data) { - const wantedDate = moment().subtract(numDaysAgo, "days").format("YYYY-MM-DD"); + const wantedDate = moment().subtract(numDaysAgo, "days").locale("en").format("YYYY-MM-DD"); const item = this.data.find(d => d.x === wantedDate); if (item) { return item.y; @@ -20,7 +31,7 @@ const Report = Discourse.Model.extend({ if (this.data) { const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day"); const latestDate = moment().subtract(startDaysAgo, "days").startOf("day"); - var d, sum = 0, count = 0; + let d, sum = 0, count = 0; _.each(this.data, datum => { d = moment(datum.x); if (d >= earliestDate && d <= latestDate) { @@ -33,105 +44,208 @@ const Report = Discourse.Model.extend({ } }, - todayCount: function() { return this.valueAt(0); }.property("data"), - yesterdayCount: function() { return this.valueAt(1); }.property("data"), - sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data"), - thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data"), + todayCount: function() { return this.valueAt(0); }.property("data", "average"), + yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"), + sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"), + thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"), - lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"), - lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"), + lastSevenDaysCount: function() { + return this.averageCount(7, this.valueFor(1, 7)); + }.property("data", "average"), + lastThirtyDaysCount: function() { + return this.averageCount(30, this.valueFor(1, 30)); + }.property("data", "average"), - yesterdayTrend: function() { - const yesterdayVal = this.valueAt(1); - const twoDaysAgoVal = this.valueAt(2); - if (yesterdayVal > twoDaysAgoVal) { - return "trending-up"; - } else if (yesterdayVal < twoDaysAgoVal) { - return "trending-down"; - } else { - return "no-change"; + averageCount(count, value) { + return this.get("average") ? value / count : value; + }, + + @computed("yesterdayCount", "higher_is_better") + yesterdayTrend(yesterdayCount, higherIsBetter) { + return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter); + }, + + @computed("lastSevenDaysCount", "higher_is_better") + sevenDaysTrend(lastSevenDaysCount, higherIsBetter) { + return this._computeTrend(this.valueFor(8, 14), lastSevenDaysCount, higherIsBetter); + }, + + @computed("data") + currentTotal(data){ + return _.reduce(data, (cur, pair) => cur + pair.y, 0); + }, + + @computed("data", "currentTotal") + currentAverage(data, total) { + return Ember.makeArray(data).length === 0 ? 0 : parseFloat((total / parseFloat(data.length)).toFixed(1)); + }, + + @computed("trend", "higher_is_better") + trendIcon(trend, higherIsBetter) { + return this._iconForTrend(trend, higherIsBetter); + }, + + @computed("sevenDaysTrend", "higher_is_better") + sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) { + return this._iconForTrend(sevenDaysTrend, higherIsBetter); + }, + + @computed("thirtyDaysTrend", "higher_is_better") + thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) { + return this._iconForTrend(thirtyDaysTrend, higherIsBetter); + }, + + @computed("yesterdayTrend", "higher_is_better") + yesterdayTrendIcon(yesterdayTrend, higherIsBetter) { + return this._iconForTrend(yesterdayTrend, higherIsBetter); + }, + + @computed("prev_period", "currentTotal", "currentAverage", "higher_is_better") + trend(prev, currentTotal, currentAverage, higherIsBetter) { + const total = this.get("average") ? currentAverage : currentTotal; + return this._computeTrend(prev, total, higherIsBetter); + }, + + @computed("prev30Days", "lastThirtyDaysCount", "higher_is_better") + thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) { + return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter); + }, + + @computed("type") + icon(type) { + if (type.indexOf("message") > -1) { + return "envelope"; } - }.property("data"), - - sevenDayTrend: function() { - const currentPeriod = this.valueFor(1, 7); - const prevPeriod = this.valueFor(8, 14); - if (currentPeriod > prevPeriod) { - return "trending-up"; - } else if (currentPeriod < prevPeriod) { - return "trending-down"; - } else { - return "no-change"; - } - }.property("data"), - - thirtyDayTrend: function() { - if (this.get("prev30Days")) { - const currentPeriod = this.valueFor(1, 30); - if (currentPeriod > this.get("prev30Days")) { - return "trending-up"; - } else if (currentPeriod < this.get("prev30Days")) { - return "trending-down"; - } - } - return "no-change"; - }.property("data", "prev30Days"), - - icon: function() { - switch (this.get("type")) { + switch (type) { + case "page_view_total_reqs": return "file"; + case "visits": return "user"; + case "time_to_first_response": return "reply"; case "flags": return "flag"; case "likes": return "heart"; case "bookmarks": return "bookmark"; default: return null; } - }.property("type"), + }, - method: function() { - if (this.get("type") === "time_to_first_response") { + @computed("type") + method(type) { + if (type === "time_to_first_response") { return "average"; } else { return "sum"; } - }.property("type"), + }, percentChangeString(val1, val2) { - const val = ((val1 - val2) / val2) * 100; - if (isNaN(val) || !isFinite(val)) { + const change = this._computeChange(val1, val2); + + if (isNaN(change) || !isFinite(change)) { return null; - } else if (val > 0) { - return "+" + val.toFixed(0) + "%"; + } else if (change > 0) { + return "+" + change.toFixed(0) + "%"; } else { - return val.toFixed(0) + "%"; + return change.toFixed(0) + "%"; } }, - changeTitle(val1, val2, prevPeriodString) { - const percentChange = this.percentChangeString(val1, val2); - var title = ""; - if (percentChange) { title += percentChange + " change. "; } - title += "Was " + val2 + " " + prevPeriodString + "."; + @computed("prev_period", "currentTotal", "currentAverage") + trendTitle(prev, currentTotal, currentAverage) { + let current = this.get("average") ? currentAverage : currentTotal; + let percent = this.percentChangeString(prev, current); + + if (this.get("average")) { + prev = prev ? prev.toFixed(1) : "0"; + if (this.get("percent")) { + current += "%"; + prev += "%"; + } + } else { + prev = number(prev); + current = number(current); + } + + return I18n.t("admin.dashboard.reports.trend_title", {percent, prev, current}); + }, + + changeTitle(valAtT1, valAtT2, prevPeriodString) { + const change = this.percentChangeString(valAtT1, valAtT2); + let title = ""; + if (change) { title += `${change} change. `; } + title += `Was ${number(valAtT1)} ${prevPeriodString}.`; return title; }, - yesterdayCountTitle: function() { - return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago"); - }.property("data"), + @computed("yesterdayCount") + yesterdayCountTitle(yesterdayCount) { + return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago"); + }, - sevenDayCountTitle: function() { - return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago"); - }.property("data"), + @computed("lastSevenDaysCount") + sevenDaysCountTitle(lastSevenDaysCount) { + return this.changeTitle(this.valueFor(8, 14), lastSevenDaysCount, "two weeks ago"); + }, - thirtyDayCountTitle: function() { - return this.changeTitle(this.valueFor(1, 30), this.get("prev30Days"), "in the previous 30 day period"); - }.property("data"), + @computed("prev30Days", "lastThirtyDaysCount") + thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) { + return this.changeTitle(prev30Days, lastThirtyDaysCount, "in the previous 30 day period"); + }, - dataReversed: function() { - return this.get("data").toArray().reverse(); - }.property("data") + @computed("data") + sortedData(data) { + return this.get("xAxisIsDate") ? data.toArray().reverse() : data.toArray(); + }, + @computed("data") + xAxisIsDate() { + if (!this.data[0]) return false; + return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); + }, + + _computeChange(valAtT1, valAtT2) { + return ((valAtT2 - valAtT1) / valAtT1) * 100; + }, + + _computeTrend(valAtT1, valAtT2, higherIsBetter) { + const change = this._computeChange(valAtT1, valAtT2); + + if (change > 50) { + return higherIsBetter ? "high-trending-up" : "high-trending-down"; + } else if (change > 2) { + return higherIsBetter ? "trending-up" : "trending-down"; + } else if (change <= 2 && change >= -2) { + return "no-change"; + } else if (change < -50) { + return higherIsBetter ? "high-trending-down" : "high-trending-up"; + } else if (change < -2) { + return higherIsBetter ? "trending-down" : "trending-up"; + } + }, + + _iconForTrend(trend, higherIsBetter) { + switch (trend) { + case "trending-up": + return higherIsBetter ? "angle-up" : "angle-down"; + case "trending-down": + return higherIsBetter ? "angle-down" : "angle-up"; + case "high-trending-up": + return higherIsBetter ? "angle-double-up" : "angle-double-down"; + case "high-trending-down": + return higherIsBetter ? "angle-double-down" : "angle-double-up"; + default: + return null; + } + } }); Report.reopenClass({ + fillMissingDates(report) { + if (_.isArray(report.data)) { + + const startDateFormatted = moment.utc(report.start_date).locale("en").format("YYYY-MM-DD"); + const endDateFormatted = moment.utc(report.end_date).locale("en").format("YYYY-MM-DD"); + report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted); + } + }, find(type, startDate, endDate, categoryId, groupId) { return ajax("/admin/reports/" + type, { @@ -142,16 +256,19 @@ Report.reopenClass({ group_id: groupId } }).then(json => { - // Add a percent field to each tuple - let maxY = 0; - json.report.data.forEach(row => { - if (row.y > maxY) maxY = row.y; - }); - if (maxY > 0) { - json.report.data.forEach(row => row.percentage = Math.round((row.y / maxY) * 100)); - } + // Add zero values for missing dates + Report.fillMissingDates(json.report); + const model = Report.create({ type: type }); model.setProperties(json.report); + + if (json.report.related_report) { + // TODO: fillMissingDates if xaxis is date + const related = Report.create({ type: json.report.related_report.type }); + related.setProperties(json.report.related_report); + model.set("relatedReport", related); + } + return model; }); } diff --git a/app/assets/javascripts/admin/models/site-setting.js.es6 b/app/assets/javascripts/admin/models/site-setting.js.es6 index 6ed78787968..3fc3cf43f5c 100644 --- a/app/assets/javascripts/admin/models/site-setting.js.es6 +++ b/app/assets/javascripts/admin/models/site-setting.js.es6 @@ -1,31 +1,7 @@ import { ajax } from 'discourse/lib/ajax'; -const SiteSetting = Discourse.Model.extend({ - overridden: function() { - let val = this.get('value'), - defaultVal = this.get('default'); +import Setting from 'admin/mixins/setting-object'; - if (val === null) val = ''; - if (defaultVal === null) defaultVal = ''; - - return val.toString() !== defaultVal.toString(); - }.property('value', 'default'), - - validValues: function() { - const vals = [], - translateNames = this.get('translate_names'); - - this.get('valid_values').forEach(function(v) { - if (v.name && v.name.length > 0) { - vals.addObject(translateNames ? {name: I18n.t(v.name), value: v.value} : v); - } - }); - return vals; - }.property('valid_values'), - - allowsNone: function() { - if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.site_settings.none'; - }.property('valid_values') -}); +const SiteSetting = Discourse.Model.extend(Setting, {}); SiteSetting.reopenClass({ findAll() { diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 index e60d815f7c7..88cba94b4c8 100644 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6 @@ -1,50 +1,58 @@ -import { ajax } from 'discourse/lib/ajax'; -import AdminUser from 'admin/models/admin-user'; -import { escapeExpression } from 'discourse/lib/utilities'; +import computed from "ember-addons/ember-computed-decorators"; +import { ajax } from "discourse/lib/ajax"; +import AdminUser from "admin/models/admin-user"; +import { escapeExpression } from "discourse/lib/utilities"; + +function format(label, value, escape = true) { + return value ? `${I18n.t(label)}: ${escape ? escapeExpression(value) : value}` : ""; +}; const StaffActionLog = Discourse.Model.extend({ showFullDetails: false, - actionName: function() { - return I18n.t("admin.logs.staff_actions.actions." + this.get('action_name')); - }.property('action_name'), - - formattedDetails: function() { - var formatted = ""; - formatted += this.format('email', 'email'); - formatted += this.format('admin.logs.ip_address', 'ip_address'); - formatted += this.format('admin.logs.topic_id', 'topic_id'); - formatted += this.format('admin.logs.post_id', 'post_id'); - formatted += this.format('admin.logs.category_id', 'category_id'); - if (!this.get('useCustomModalForDetails')) { - formatted += this.format('admin.logs.staff_actions.new_value', 'new_value'); - formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value'); - } - if (!this.get('useModalForDetails')) { - if (this.get('details')) formatted += escapeExpression(this.get('details')) + '
'; - } - return formatted; - }.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'), - - format: function(label, propertyName) { - if (this.get(propertyName)) { - return ('' + I18n.t(label) + ': ' + escapeExpression(this.get(propertyName)) + '
'); - } else { - return ''; - } + @computed("action_name") + actionName(actionName) { + return I18n.t(`admin.logs.staff_actions.actions.${actionName}`); }, - useModalForDetails: function() { - return (this.get('details') && this.get('details').length > 100); - }.property('action_name'), + @computed("email", "ip_address", "topic_id", "post_id", "category_id", "new_value", "previous_value", "details", "useCustomModalForDetails", "useModalForDetails") + formattedDetails(email, ipAddress, topicId, postId, categoryId, newValue, previousValue, details, useCustomModalForDetails, useModalForDetails) { + const postLink = postId ? `${postId}` : null; - useCustomModalForDetails: function() { - return _.contains(['change_theme', 'delete_theme'], this.get('action_name')); - }.property('action_name') + let lines = [ + format("email", email), + format("admin.logs.ip_address", ipAddress), + format("admin.logs.topic_id", topicId), + format("admin.logs.post_id", postLink, false), + format("admin.logs.category_id", categoryId), + ]; + + if (!useCustomModalForDetails) { + lines.push(format("admin.logs.staff_actions.new_value", newValue)); + lines.push(format("admin.logs.staff_actions.previous_value", previousValue)); + } + + if (!useModalForDetails && details) { + lines = [...lines, ...escapeExpression(details).split("\n")]; + } + + const formatted = lines.filter(l => l.length > 0).join("
"); + return formatted.length > 0 ? formatted + "
" : ""; + }, + + @computed("details") + useModalForDetails(details) { + return details && details.length > 100; + }, + + @computed("action_name") + useCustomModalForDetails(actionName) { + return ["change_theme", "delete_theme"].includes(actionName); + } }); StaffActionLog.reopenClass({ - create: function(attrs) { + create(attrs) { attrs = attrs || {}; if (attrs.acting_user) { @@ -56,13 +64,11 @@ StaffActionLog.reopenClass({ return this._super(attrs); }, - findAll: function(filters) { - return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then((data) => { + findAll(data) { + return ajax("/admin/logs/staff_action_logs.json", { data }).then(result => { return { - staff_action_logs: data.staff_action_logs.map(function(s) { - return StaffActionLog.create(s); - }), - user_history_actions: data.user_history_actions + staff_action_logs: result.staff_action_logs.map(s => StaffActionLog.create(s)), + user_history_actions: result.user_history_actions }; }); } diff --git a/app/assets/javascripts/admin/models/theme-settings.js.es6 b/app/assets/javascripts/admin/models/theme-settings.js.es6 new file mode 100644 index 00000000000..b1e824cf493 --- /dev/null +++ b/app/assets/javascripts/admin/models/theme-settings.js.es6 @@ -0,0 +1,3 @@ +import Setting from 'admin/mixins/setting-object'; + +export default Discourse.Model.extend(Setting, {}); diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index 742d06317fd..d7c5a89fd05 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -5,6 +5,8 @@ const THEME_UPLOAD_VAR = 2; const Theme = RestModel.extend({ + FIELDS_IDS: [0, 1], + @computed('theme_fields') themeFields(fields) { @@ -14,13 +16,11 @@ const Theme = RestModel.extend({ } let hash = {}; - if (fields) { - fields.forEach(field=>{ - if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) { - hash[this.getKey(field)] = field; - } - }); - } + fields.forEach(field => { + if (!field.type_id || this.get('FIELDS_IDS').includes(field.type_id)) { + hash[this.getKey(field)] = field; + } + }); return hash; }, @@ -29,11 +29,11 @@ const Theme = RestModel.extend({ if (!fields) { return []; } - return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR); + return fields.filter(f => f.target === 'common' && f.type_id === THEME_UPLOAD_VAR); }, getKey(field){ - return field.target + " " + field.name; + return `${field.target} ${field.name}`; }, hasEdited(target, name){ @@ -151,6 +151,11 @@ const Theme = RestModel.extend({ .then(() => this.set("changed", false)); }, + saveSettings(name, value) { + const settings = {}; + settings[name] = value; + return this.save({ settings }); + } }); export default Theme; diff --git a/app/assets/javascripts/admin/models/tl3-requirements.js.es6 b/app/assets/javascripts/admin/models/tl3-requirements.js.es6 index ac27b095a7b..6dc655b1ced 100644 --- a/app/assets/javascripts/admin/models/tl3-requirements.js.es6 +++ b/app/assets/javascripts/admin/models/tl3-requirements.js.es6 @@ -1,11 +1,15 @@ -const TL3Requirements = Discourse.Model.extend({ - days_visited_percent: function() { - return Math.round((this.get('days_visited') * 100) / this.get('time_period')); - }.property('days_visited', 'time_period'), +import computed from 'ember-addons/ember-computed-decorators'; - min_days_visited_percent: function() { - return Math.round((this.get('min_days_visited') * 100) / this.get('time_period')); - }.property('min_days_visited', 'time_period'), +export default Discourse.Model.extend({ + @computed('days_visited', 'time_period') + days_visited_percent(daysVisited, timePeriod) { + return Math.round((daysVisited * 100) / timePeriod); + }, + + @computed('min_days_visited', 'time_period') + min_days_visited_percent(minDaysVisited, timePeriod) { + return Math.round((minDaysVisited * 100) / timePeriod); + }, met: function() { return { @@ -21,22 +25,26 @@ const TL3Requirements = Discourse.Model.extend({ likes_received: this.get('num_likes_received') >= this.get('min_likes_received'), likes_received_days: this.get('num_likes_received_days') >= this.get('min_likes_received_days'), likes_received_users: this.get('num_likes_received_users') >= this.get('min_likes_received_users'), - level_locked: this.get('trust_level_locked') + level_locked: this.get('trust_level_locked'), + silenced: this.get('penalty_counts.silenced') === 0, + suspended: this.get('penalty_counts.suspended') === 0 }; - }.property('days_visited', 'min_days_visited', - 'num_topics_replied_to', 'min_topics_replied_to', - 'topics_viewed', 'min_topics_viewed', - 'posts_read', 'min_posts_read', - 'num_flagged_posts', 'max_flagged_posts', - 'topics_viewed_all_time', 'min_topics_viewed_all_time', - 'posts_read_all_time', 'min_posts_read_all_time', - 'num_flagged_by_users', 'max_flagged_by_users', - 'num_likes_given', 'min_likes_given', - 'num_likes_received', 'min_likes_received', - 'num_likes_received', 'min_likes_received', - 'num_likes_received_days', 'min_likes_received_days', - 'num_likes_received_users', 'min_likes_received_users', - 'trust_level_locked') + }.property( + 'days_visited', 'min_days_visited', + 'num_topics_replied_to', 'min_topics_replied_to', + 'topics_viewed', 'min_topics_viewed', + 'posts_read', 'min_posts_read', + 'num_flagged_posts', 'max_flagged_posts', + 'topics_viewed_all_time', 'min_topics_viewed_all_time', + 'posts_read_all_time', 'min_posts_read_all_time', + 'num_flagged_by_users', 'max_flagged_by_users', + 'num_likes_given', 'min_likes_given', + 'num_likes_received', 'min_likes_received', + 'num_likes_received', 'min_likes_received', + 'num_likes_received_days', 'min_likes_received_days', + 'num_likes_received_users', 'min_likes_received_users', + 'trust_level_locked', + 'penalty_counts.silenced', + 'penalty_counts.suspended' + ) }); - -export default TL3Requirements; diff --git a/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 b/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 index fce0b126d36..8c77fdd1aef 100644 --- a/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups-index.js.es6 @@ -1,7 +1,15 @@ import Backup from 'admin/models/backup'; export default Ember.Route.extend({ + activate() { + this.messageBus.subscribe("/admin/backups", backups => this.controller.set("model", backups)); + }, + model() { return Backup.find(); + }, + + deactivate() { + this.messageBus.unsubscribe("/admin/backups"); } }); diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index 92ac4652fa6..f992b5fc93f 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -9,26 +9,24 @@ const LOG_CHANNEL = "/admin/backups/logs"; export default Discourse.Route.extend({ activate() { - this.messageBus.subscribe(LOG_CHANNEL, this._processLogMessage.bind(this)); - }, - - _processLogMessage(log) { - if (log.message === "[STARTED]") { - this.controllerFor("adminBackups").set("model.isOperationRunning", true); - this.controllerFor("adminBackupsLogs").get('logs').clear(); - } else if (log.message === "[FAILED]") { - this.controllerFor("adminBackups").set("model.isOperationRunning", false); - bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation })); - } else if (log.message === "[SUCCESS]") { - Discourse.User.currentProp("hideReadOnlyAlert", false); - this.controllerFor("adminBackups").set("model.isOperationRunning", false); - if (log.operation === "restore") { - // redirect to homepage when the restore is done (session might be lost) - window.location.pathname = Discourse.getURL("/"); + this.messageBus.subscribe(LOG_CHANNEL, (log) => { + if (log.message === "[STARTED]") { + this.controllerFor("adminBackups").set("model.isOperationRunning", true); + this.controllerFor("adminBackupsLogs").get('logs').clear(); + } else if (log.message === "[FAILED]") { + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation })); + } else if (log.message === "[SUCCESS]") { + Discourse.User.currentProp("hideReadOnlyAlert", false); + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + if (log.operation === "restore") { + // redirect to homepage when the restore is done (session might be lost) + window.location.pathname = Discourse.getURL("/"); + } + } else { + this.controllerFor("adminBackupsLogs").get('logs').pushObject(Em.Object.create(log)); } - } else { - this.controllerFor("adminBackupsLogs").get('logs').pushObject(Em.Object.create(log)); - } + }); }, model() { @@ -122,12 +120,7 @@ export default Discourse.Route.extend({ }, uploadSuccess(filename) { - const self = this; - bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename }), function() { - Backup.find().then(function (backups) { - self.controllerFor("adminBackupsIndex").set("model", backups); - }); - }); + bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename })); }, uploadError(filename, message) { diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 index c1d3b225ffd..d9e7e6249fd 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 @@ -18,6 +18,11 @@ export default Ember.Route.extend({ }, setupController(controller, wrapper) { + const fields = controller.fieldsForTarget(wrapper.target); + if (!fields.includes(wrapper.field_name)) { + this.transitionTo('adminCustomizeThemes.edit', wrapper.model.id, wrapper.target, fields[0]); + return; + } controller.set("model", wrapper.model); controller.setTargetName(wrapper.target || "common"); controller.set("fieldName", wrapper.field_name || "scss"); diff --git a/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 new file mode 100644 index 00000000000..30ca9b033c0 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-dashboard-next.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + activate() { + this.controllerFor('admin-dashboard-next').fetchDashboard(); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-group.js.es6 b/app/assets/javascripts/admin/routes/admin-group.js.es6 deleted file mode 100644 index 0009f834e13..00000000000 --- a/app/assets/javascripts/admin/routes/admin-group.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -import Group from 'discourse/models/group'; - -export default Discourse.Route.extend({ - - model(params) { - if (params.name === 'new') { - return Group.create({ automatic: false, visibility_level: 0 }); - } - - const group = this.modelFor('adminGroupsType').findBy('name', params.name); - - if (!group) { return this.transitionTo('adminGroups.index'); } - - return group; - }, - - setupController(controller, model) { - controller.set("model", model); - controller.set("model.usernames", null); - controller.set("savingStatus", ""); - model.findMembers(); - } - -}); diff --git a/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 deleted file mode 100644 index 8d9554556f8..00000000000 --- a/app/assets/javascripts/admin/routes/admin-groups-bulk.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import Group from 'discourse/models/group'; - -export default Ember.Route.extend({ - model() { - return Group.findAll().then(groups => { - return groups.filter(g => !g.get('automatic')); - }); - }, - - setupController(controller, groups) { - controller.setProperties({ groups, groupId: null, users: null }); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-groups-index.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-index.js.es6 deleted file mode 100644 index e650d150fb7..00000000000 --- a/app/assets/javascripts/admin/routes/admin-groups-index.js.es6 +++ /dev/null @@ -1,5 +0,0 @@ -export default Discourse.Route.extend({ - redirect: function() { - this.transitionTo("adminGroupsType", "custom"); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-groups-type.js.es6 b/app/assets/javascripts/admin/routes/admin-groups-type.js.es6 deleted file mode 100644 index 52e383bf9a0..00000000000 --- a/app/assets/javascripts/admin/routes/admin-groups-type.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -import Group from 'discourse/models/group'; - -export default Discourse.Route.extend({ - model(params) { - this.set("type", params.type); - return Group.findAll().then(function(groups) { - return groups.filterBy("type", params.type); - }); - }, - - setupController(controller, model){ - controller.set("type", this.get("type")); - controller.set("model", model); - } -}); diff --git a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 index 820c5207d44..8c5886f4a43 100644 --- a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 @@ -8,12 +8,15 @@ export default Ember.Route.extend({ const controller = this.controllerFor('adminSiteSettings'); this.transitionTo('adminSiteSettingsCategory', 'plugins').then(() => { if (plugin) { + const siteSettingFilter = plugin.get('enabled_setting_filter'); const match = /^(.*)_enabled/.exec(plugin.get('enabled_setting')); - if (match[1]) { + const filter = siteSettingFilter || match[1]; + + if (filter) { // filterContent() is normally on a debounce from typing. // Because we don't want the default of "All Results", we tell it // to skip the next debounce. - controller.set('filter', match[1]); + controller.set('filter', filter); controller.set('_skipBounce', true); controller.filterContentNow('plugins'); } @@ -22,4 +25,3 @@ export default Ember.Route.extend({ } } }); - diff --git a/app/assets/javascripts/admin/routes/admin-reports.js.es6 b/app/assets/javascripts/admin/routes/admin-reports.js.es6 index 65771110e86..acddcff5d61 100644 --- a/app/assets/javascripts/admin/routes/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-reports.js.es6 @@ -12,8 +12,8 @@ export default Discourse.Route.extend({ model: model, categoryId: (model.get('category_id') || 'all'), groupId: model.get('group_id'), - startDate: moment(model.get('start_date')).format('YYYY-MM-DD'), - endDate: moment(model.get('end_date')).format('YYYY-MM-DD') + startDate: moment(model.get('start_date')).utc().format('YYYY-MM-DD'), + endDate: moment(model.get('end_date')).utc().format('YYYY-MM-DD') }); } }); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index a6ccabc810e..5a033e1efa5 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -1,6 +1,7 @@ export default function() { this.route('admin', { resetNamespace: true }, function() { - this.route('dashboard', { path: '/' }); + this.route('dashboard', { path: '/dashboard-old' }); + this.route('dashboardNext', { path: '/' }); this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() { this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} ); }); @@ -76,14 +77,6 @@ export default function() { }); }); - this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() { - this.route('bulk'); - this.route('bulkComplete', { path: 'bulk-complete' }); - this.route('adminGroupsType', { path: '/:type', resetNamespace: true }, function() { - this.route('adminGroup', { path: '/:name', resetNamespace: true }); - }); - }); - this.route('adminUsers', { path: '/users', resetNamespace: true }, function() { this.route('adminUser', { path: '/:user_id/:username', resetNamespace: true }, function() { this.route('badges'); diff --git a/app/assets/javascripts/admin/routes/admin-search-logs-term.js.es6 b/app/assets/javascripts/admin/routes/admin-search-logs-term.js.es6 index 775fec62c5e..80c5117dc41 100644 --- a/app/assets/javascripts/admin/routes/admin-search-logs-term.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-search-logs-term.js.es6 @@ -1,4 +1,6 @@ import { ajax } from 'discourse/lib/ajax'; +import { fillMissingDates } from 'discourse/lib/utilities'; +import { translateResults } from "discourse/lib/search"; export default Discourse.Route.extend({ queryParams: { @@ -15,6 +17,16 @@ export default Discourse.Route.extend({ search_type: params.searchType } }).then(json => { + // Add zero values for missing dates + if (json.term.data.length > 0) { + const startDate = (json.term.period === "all") ? moment(json.term.data[0].x).format('YYYY-MM-DD') : moment(json.term.start_date).format('YYYY-MM-DD'); + const endDate = moment(json.term.end_date).format('YYYY-MM-DD'); + json.term.data = fillMissingDates(json.term.data, startDate, endDate); + } + if (json.term.search_result) { + json.term.search_result = translateResults(json.term.search_result); + } + const model = Ember.Object.create({ type: "search_log_term" }); model.setProperties(json.term); return model; diff --git a/app/assets/javascripts/admin/routes/admin-user-index.js.es6 b/app/assets/javascripts/admin/routes/admin-user-index.js.es6 index 36d1d8cedd0..930b45c8b14 100644 --- a/app/assets/javascripts/admin/routes/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-user-index.js.es6 @@ -21,15 +21,5 @@ export default Discourse.Route.extend({ availableGroups: this._availableGroups, model }); - }, - - actions: { - viewActionLogs(username) { - const controller = this.controllerFor('adminLogs.staffActionLogs'); - this.transitionTo('adminLogs.staffActionLogs').then(() => { - controller.set('filters', Ember.Object.create()); - controller._changeFilters({ target_user: username }); - }); - } } }); diff --git a/app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js.es6 b/app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js.es6 index 40c874eaaa1..cceaf9fd478 100644 --- a/app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-user-tl3-requirements.js.es6 @@ -1,5 +1,5 @@ export default Discourse.Route.extend({ - model: function() { + model() { return this.modelFor('adminUser'); } }); diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6 index 439b85cd421..9f9bec8b036 100644 --- a/app/assets/javascripts/admin/services/admin-tools.js.es6 +++ b/app/assets/javascripts/admin/services/admin-tools.js.es6 @@ -17,10 +17,26 @@ export default Ember.Service.extend({ this.siteSettings = getOwner(this).lookup('site-settings:main'); }, + showActionLogs(target, filters) { + const controller = getOwner(target).lookup('controller:adminLogs.staffActionLogs'); + target.transitionToRoute('adminLogs.staffActionLogs').then(() => { + controller.set('filters', Ember.Object.create()); + controller._changeFilters(filters); + }); + }, + + showFlagsReceived(user) { + showModal(`admin-flags-received`, { admin: true, model: user }); + }, + checkSpammer(userId) { return AdminUser.find(userId).then(au => this.spammerDetails(au)); }, + deleteUser(id) { + AdminUser.find(id).then(user => user.destroy({ deletePosts: true })); + }, + spammerDetails(adminUser) { return { deleteUser: () => this._deleteSpammer(adminUser), @@ -36,17 +52,20 @@ export default Ember.Service.extend({ modalClass: `${type}-user-modal` }); if (opts.post) { - controller.set('post', opts.post); + controller.setProperties({ + post: opts.post, + postEdit: opts.post.get('raw') + }); } - let promise = user.adminUserView ? + return (user.adminUserView ? Ember.RSVP.resolve(user) : - AdminUser.find(user.get('id')); - - promise.then(loadedUser => { + AdminUser.find(user.get('id')) + ).then(loadedUser => { controller.setProperties({ user: loadedUser, loadingUser: false, + before: opts.before, successCallback: opts.successCallback }); }); diff --git a/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl b/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl new file mode 100644 index 00000000000..23cc50b1f35 Binary files /dev/null and b/app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl differ diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index af4aafde947..cf4d231d4ed 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -3,7 +3,7 @@
diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index 5cfd1231144..33229e22534 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -1,12 +1,12 @@
-
+
+
{{#if model.canRollback}} {{d-button action="rollback" diff --git a/app/assets/javascripts/admin/templates/badges-index.hbs b/app/assets/javascripts/admin/templates/badges-index.hbs index 674eae8de29..4a71e7d69a0 100644 --- a/app/assets/javascripts/admin/templates/badges-index.hbs +++ b/app/assets/javascripts/admin/templates/badges-index.hbs @@ -1,4 +1,4 @@ -{{#d-section class="current-badge span13"}} +{{#d-section class="current-badge content-body"}}

{{i18n 'admin.badges.none_selected'}}

diff --git a/app/assets/javascripts/admin/templates/badges-show.hbs b/app/assets/javascripts/admin/templates/badges-show.hbs index 94239f9d03c..7436c48898f 100644 --- a/app/assets/javascripts/admin/templates/badges-show.hbs +++ b/app/assets/javascripts/admin/templates/badges-show.hbs @@ -1,4 +1,4 @@ -{{#d-section class="current-badge span13"}} +{{#d-section class="current-badge content-body"}}
@@ -18,7 +18,7 @@
{{input type="text" name="image" value=buffered.image}} -

{{i18n 'admin.badges.icon_help'}}

+

{{i18n 'admin.badges.image_help'}}

@@ -144,7 +144,7 @@ {{/d-section}} {{#if grant_count}} -
+
{{#link-to 'badges.show' this}}{{i18n 'badges.granted' count=grant_count}}{{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/badges.hbs b/app/assets/javascripts/admin/templates/badges.hbs index aeeaac9c9e2..b5789d4c42a 100644 --- a/app/assets/javascripts/admin/templates/badges.hbs +++ b/app/assets/javascripts/admin/templates/badges.hbs @@ -1,6 +1,6 @@
-
+

{{i18n 'admin.badges.title'}}

    {{#each model as |badge|}} diff --git a/app/assets/javascripts/admin/templates/components/admin-nav.hbs b/app/assets/javascripts/admin/templates/components/admin-nav.hbs index c5b1d3b58a7..eb705949e54 100644 --- a/app/assets/javascripts/admin/templates/components/admin-nav.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-nav.hbs @@ -1,7 +1,7 @@
    -
    +
    +
    diff --git a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs index 82fbf0d7996..d7f37040f67 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report-counts.hbs @@ -5,18 +5,20 @@ {{report.title}} -{{number report.todayCount}} +{{number report.todayCount ceil=true}} - {{number report.yesterdayCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}} + {{number report.yesterdayCount ceil=true}} {{d-icon report.yesterdayTrendIcon}} - - {{number report.lastSevenDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}} + + {{number report.lastSevenDaysCount ceil=true}} {{d-icon report.sevenDaysTrendIcon}} - - {{number report.lastThirtyDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}} + + {{number report.lastThirtyDaysCount ceil=true}} {{d-icon report.thirtyDaysTrendIcon}} -{{number report.total}} +{{#if allTime}} + {{number report.total}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/admin-table-report.hbs b/app/assets/javascripts/admin/templates/components/admin-table-report.hbs new file mode 100644 index 00000000000..53ea2e51c6e --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-table-report.hbs @@ -0,0 +1,17 @@ +{{#if model.sortedData}} + + + + + + + {{#each model.sortedData as |row|}} + + + + + {{/each}} +
    {{model.xaxis}}{{model.yaxis}}
    {{row.x}} + {{row.y}} +
    +{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs b/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs index 419ef0b69cf..99f1de8ce24 100644 --- a/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs @@ -4,11 +4,11 @@ {{/admin-form-row}} {{#admin-form-row label="admin.user_fields.name"}} - {{input value=buffered.name class="user-field-name"}} + {{input value=buffered.name class="user-field-name" maxlength="255"}} {{/admin-form-row}} {{#admin-form-row label="admin.user_fields.description"}} - {{input value=buffered.description class="user-field-desc"}} + {{input value=buffered.description class="user-field-desc" maxlength="255"}} {{/admin-form-row}} {{#if bufferedFieldType.hasOptions}} diff --git a/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs new file mode 100644 index 00000000000..9e976f46d26 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/dashboard-inline-table.hbs @@ -0,0 +1,27 @@ +{{#conditional-loading-section isLoading=isLoading}} +
    +

    {{title}}

    +
    + + {{#each reportsForPeriod as |report|}} +
    + {{#unless hasBlock}} + {{#each report.data as |data|}} + + + {{#if data.icon}} + {{d-icon data.icon}} + {{/if}} + {{data.x}} + + + {{number data.y}} + + + {{/each}} + {{else}} + {{yield (hash report=report)}} + {{/unless}} +
    + {{/each}} +{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs b/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs new file mode 100644 index 00000000000..62e956cd8ca --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/dashboard-mini-chart.hbs @@ -0,0 +1,33 @@ +{{#conditional-loading-section isLoading=isLoading}} + {{#each reportsForPeriod as |report|}} +
    +

    + + {{report.title}} + + + + {{d-icon "question-circle"}} + +

    + +
    + + {{#if report.average}} + {{number report.currentAverage ceil=true}}{{#if report.percent}}%{{/if}} + {{else}} + {{number report.currentTotal ceil=true noTitle="true"}}{{#if report.percent}}%{{/if}} + {{/if}} + + + {{#if report.trendIcon}} + {{d-icon report.trendIcon class="trend-icon"}} + {{/if}} +
    +
    + {{/each}} + +
    + +
    +{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/components/dashboard-table.hbs b/app/assets/javascripts/admin/templates/components/dashboard-table.hbs new file mode 100644 index 00000000000..29ab8332cb7 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/dashboard-table.hbs @@ -0,0 +1,36 @@ +{{#conditional-loading-section isLoading=isLoading}} +
    +

    {{title}}

    +
    + + {{#each reportsForPeriod as |report|}} +
    + + + + {{#if report.labels}} + {{#each report.labels as |label|}} + + {{/each}} + {{else}} + {{#each report.data as |data|}} + + {{/each}} + {{/if}} + + + + {{#unless hasBlock}} + {{#each report.data as |data|}} + + + + {{/each}} + {{else}} + {{yield (hash report=report)}} + {{/unless}} + +
    {{label}}{{data.x}}
    {{number data.y}}
    +
    + {{/each}} +{{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs b/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs new file mode 100644 index 00000000000..7c793b98bcf --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs @@ -0,0 +1,32 @@ +
    +
    + {{i18n "admin.flags.flagged_by"}} +
    +
    + {{#each flaggedPost.post_actions as |postAction|}} + {{#flag-user user=postAction.user date=postAction.created_at}} +
    + {{post-action-title postAction.post_action_type_id postAction.name_key}} +
    + {{/flag-user}} + {{/each}} +
    +
    + +{{#if showResolvedBy}} +
    +
    + {{i18n "admin.flags.resolved_by"}} +
    +
    + {{#each flaggedPost.post_actions as |postAction|}} + {{#flag-user user=postAction.disposed_by date=postAction.disposed_at}} + {{disposition-icon postAction.disposition}} + {{#if postAction.staff_took_action}} + {{d-icon "gavel" title="admin.flags.took_action"}} + {{/if}} + {{/flag-user}} + {{/each}} +
    +
    +{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs b/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs new file mode 100644 index 00000000000..0871c5c6f1d --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/flagged-post-title.hbs @@ -0,0 +1,8 @@ +{{#if flaggedPost.topic.isPrivateMessage}} + {{d-icon "envelope"}} +{{/if}} +{{topic-status topic=flaggedPost.topic}} +{{{unbound flaggedPost.topic.fancyTitle}}} +{{#if flaggedPost.reply_count}} + {{i18n 'admin.flags.replies' count=flaggedPost.reply_count}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 2b7f03940fc..37631f1442f 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -22,17 +22,15 @@
    {{format-username flaggedPost.user.username}} + {{plugin-outlet + name="flagged-post-controls" + tagName="" + args=(hash flaggedPost=flaggedPost actableFilter=actableFilter topic=topic)}}
    {{#unless hideTitle}} -

    - {{#if flaggedPost.topic.isPrivateMessage}} - {{d-icon "envelope"}} - {{/if}} - {{topic-status topic=flaggedPost.topic}} - {{{unbound flaggedPost.topic.fancyTitle}}} -

    + {{flagged-post-title flaggedPost=flaggedPost}} {{/unless}} {{#if flaggedPost.postAuthorFlagged}} {{#if expanded}} @@ -68,46 +66,7 @@
    {{/each}} -
    -
    -
    - {{i18n "admin.flags.flagged_by"}} -
    -
    - {{#each flaggedPost.post_actions as |postAction|}} - {{#flag-user user=postAction.user date=postAction.created_at}} -
    - {{post-action-title postAction.post_action_type_id postAction.name_key}} -
    - {{/flag-user}} - {{/each}} -
    -
    - - {{#if showResolvedBy}} -
    -
    - {{i18n "admin.flags.resolved_by"}} -
    -
    - {{#each flaggedPost.post_actions as |postAction|}} - {{#flag-user user=postAction.disposed_by date=postAction.disposed_at}} - {{disposition-icon postAction.disposition}} - {{#if postAction.staff_took_action}} - {{d-icon "gavel" title="admin.flags.took_action"}} - {{/if}} - {{/flag-user}} - {{/each}} -
    -
    - {{/if}} -
    - - {{#if suspended}} -
    - The user was suspended for this post. -
    - {{/if}} + {{flag-user-lists flaggedPost=flaggedPost showResolvedBy=showResolvedBy}}
    {{#if canAct}} @@ -133,21 +92,14 @@ {{d-button class="defer-flag" - title="admin.flags.defer_flag_title" + title="admin.flags.ignore_flag_title" action="defer" icon="external-link" - label="admin.flags.defer_flag"}} + label="admin.flags.ignore_flag"}} - {{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}} - - {{#unless suspended}} - {{d-button - class="btn-danger suspend-user" - icon="ban" - label="admin.flags.suspend_user" - title="admin.flags.suspend_user_title" - action=(action "showSuspendModal")}} - {{/unless}} + {{admin-delete-flag-dropdown + post=flaggedPost + removeAfter=(action "removeAfter")}} {{/if}} {{d-button @@ -158,6 +110,6 @@ {{plugin-outlet name="flagged-post-below-controls" tagName="" - args=(hash flaggedPost=flaggedPost canAct=canAct)}} + args=(hash flaggedPost=flaggedPost canAct=canAct actableFilter=actableFilter)}}
    diff --git a/app/assets/javascripts/admin/templates/components/flagged-posts.hbs b/app/assets/javascripts/admin/templates/components/flagged-posts.hbs index a2f60c25875..5f1aab3a86e 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-posts.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-posts.hbs @@ -5,6 +5,7 @@ {{flagged-post flaggedPost=flaggedPost filter=filter + topic=topic showResolvedBy=showResolvedBy removePost=(action "removePost" flaggedPost) hideTitle=topic}} diff --git a/app/assets/javascripts/admin/templates/components/penalty-post-action.hbs b/app/assets/javascripts/admin/templates/components/penalty-post-action.hbs new file mode 100644 index 00000000000..1c8ffe56d8e --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/penalty-post-action.hbs @@ -0,0 +1,16 @@ +
    + + {{combo-box value=postAction content=penaltyActions onSelect=(action "penaltyChanged")}} +
    + +{{#if editing}} +
    + {{textarea + value=postEdit + class="post-editor"}} +
    +{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/silence-details.hbs b/app/assets/javascripts/admin/templates/components/silence-details.hbs new file mode 100644 index 00000000000..3f7c19d5ba5 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/silence-details.hbs @@ -0,0 +1,21 @@ +
    + + {{text-field + value=reason + class="silence-reason" + placeholderKey="admin.user.silence_reason_placeholder"}} +
    + + +{{textarea + value=message + class="silence-message" + placeholder=(i18n "admin.user.silence_message_placeholder")}} diff --git a/app/assets/javascripts/admin/templates/components/site-setting.hbs b/app/assets/javascripts/admin/templates/components/site-setting.hbs index 7fd80e44462..e91922f7a77 100644 --- a/app/assets/javascripts/admin/templates/components/site-setting.hbs +++ b/app/assets/javascripts/admin/templates/components/site-setting.hbs @@ -10,5 +10,5 @@ {{d-button class="cancel" action="cancel" icon="times"}}
{{else if setting.overridden}} - {{d-button action="resetDefault" icon="undo" label="admin.site_settings.reset"}} + {{d-button action="resetDefault" icon="undo" label="admin.settings.reset"}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/components/site-settings/category.hbs b/app/assets/javascripts/admin/templates/components/site-settings/category.hbs new file mode 100644 index 00000000000..e3ab41137c1 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/site-settings/category.hbs @@ -0,0 +1,3 @@ +{{category-chooser value=value allowUncategorized="true"}} +{{setting-validation-message message=validationMessage}} +
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/components/suspension-details.hbs b/app/assets/javascripts/admin/templates/components/suspension-details.hbs new file mode 100644 index 00000000000..0f2aa0fc848 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/suspension-details.hbs @@ -0,0 +1,25 @@ +
+ + {{text-field + value=reason + class="suspend-reason" + placeholderKey="admin.user.suspend_reason_placeholder"}} +
+ + +{{textarea + value=message + class="suspend-message" + placeholder=(i18n "admin.user.suspend_message_placeholder")}} diff --git a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs index 7d2b460aa07..e4b2fab801f 100644 --- a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs +++ b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs @@ -2,6 +2,6 @@ {{text-field value=word disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey=placeholderKey}} {{d-button action="submit" disabled=formSubmitted label="admin.watched_words.form.add"}} -{{#if showSuccessMessage}} - {{i18n 'admin.watched_words.form.success'}} +{{#if showMessage}} + {{message}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/customize-colors-show.hbs b/app/assets/javascripts/admin/templates/customize-colors-show.hbs index e688e133166..08cbef2ddc6 100644 --- a/app/assets/javascripts/admin/templates/customize-colors-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors-show.hbs @@ -22,7 +22,7 @@
diff --git a/app/assets/javascripts/admin/templates/customize-colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs index 920f6f19540..761996b6cb9 100644 --- a/app/assets/javascripts/admin/templates/customize-colors.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors.hbs @@ -1,4 +1,4 @@ -
+

{{i18n 'admin.customize.colors.long_title'}}

    {{#each model as |scheme|}} diff --git a/app/assets/javascripts/admin/templates/customize-email-templates.hbs b/app/assets/javascripts/admin/templates/customize-email-templates.hbs index 152c02e44a2..7dd95b092f5 100644 --- a/app/assets/javascripts/admin/templates/customize-email-templates.hbs +++ b/app/assets/javascripts/admin/templates/customize-email-templates.hbs @@ -1,5 +1,5 @@
    -
    +
      {{#each sortedTemplates as |et|}}
    • diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index 83eebb83d08..c798836b767 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -3,39 +3,47 @@

      {{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}

      {{#if error}} -
      {{error}}
      +
      {{error}}
      {{/if}}
      diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 7f339d63d74..fcd933553ca 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -50,16 +50,16 @@

      {{i18n "admin.customize.theme.css_html"}}

      {{#if hasEditedFields}} -

      {{i18n "admin.customize.theme.custom_sections"}}

      -
        - {{#each editedDescriptions as |desc|}} -
      • {{desc}}
      • - {{/each}} -
      +

      {{i18n "admin.customize.theme.custom_sections"}}

      +
        + {{#each editedDescriptions as |desc|}} +
      • {{desc}}
      • + {{/each}} +
      {{else}} -

      - {{i18n "admin.customize.theme.edit_css_html_help"}} -

      +

      + {{i18n "admin.customize.theme.edit_css_html_help"}} +

      {{/if}}

      {{#if model.remote_theme}} @@ -71,17 +71,17 @@ {{/if}} {{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}} {{#if model.remote_theme}} - - {{#if updatingRemote}} - {{i18n 'admin.customize.theme.updating'}} - {{else}} - {{#if model.remote_theme.commits_behind}} - {{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}} + + {{#if updatingRemote}} + {{i18n 'admin.customize.theme.updating'}} {{else}} - {{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}} + {{#if model.remote_theme.commits_behind}} + {{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}} + {{else}} + {{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}} + {{/if}} {{/if}} - {{/if}} - + {{/if}}

      @@ -105,6 +105,15 @@ {{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}

      + {{#if hasSettings}} +

      {{i18n "admin.customize.theme.theme_settings"}}

      + {{#d-section class="form-horizontal theme settings"}} + {{#each settings as |setting|}} + {{theme-setting setting=setting model=model class="theme-setting"}} + {{/each}} + {{/d-section}} + {{/if}} + {{#if availableChildThemes}}

      {{i18n "admin.customize.theme.theme_components"}}

      {{#unless model.childThemes.length}} diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index c9474917ef6..91644922640 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -1,10 +1,11 @@ {{#unless editingTheme}} -
      +

      {{i18n 'admin.customize.theme.long_title'}}

        {{#each sortedThemes as |theme|}}
      • {{#link-to 'adminCustomizeThemes.show' theme replace=true}} + {{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}} {{theme.name}} {{#if theme.user_selectable}} {{d-icon "user"}} diff --git a/app/assets/javascripts/admin/templates/dashboard-problems.hbs b/app/assets/javascripts/admin/templates/dashboard-problems.hbs new file mode 100644 index 00000000000..94f878bc9e9 --- /dev/null +++ b/app/assets/javascripts/admin/templates/dashboard-problems.hbs @@ -0,0 +1,36 @@ +{{#if foundProblems}} +
        +
        + {{#conditional-loading-spinner condition=loadingProblems}} +
        +

        + {{d-icon "exclamation-triangle"}} + {{i18n 'admin.dashboard.problems_found'}}

        +
          + {{#each problems as |problem|}} +
        • {{{problem}}}
        • + {{/each}} +
        +
        +

        + {{i18n 'admin.dashboard.last_checked'}}: {{problemsTimestamp}} + {{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}} +

        + {{/conditional-loading-spinner}} +
        +
        +
        +{{else}} + {{#if thereWereProblems}} +
        +
         
        +
        +

        + {{i18n 'admin.dashboard.no_problems'}} + {{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}} +

        +
        +
        +
        + {{/if}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs index 6b4dc88584a..6018b64572f 100644 --- a/app/assets/javascripts/admin/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/templates/dashboard.hbs @@ -1,11 +1,5 @@ -{{plugin-outlet name="admin-dashboard-top"}} - {{#conditional-loading-spinner condition=loading}}
        - {{#if showVersionChecks}} - {{partial 'admin/templates/version-checks'}} - {{/if}} -
        @@ -174,42 +168,6 @@
        - {{#if foundProblems}} -
        -
        {{d-icon "exclamation-triangle"}}
        -
        - {{#conditional-loading-spinner condition=loadingProblems}} -

        - {{i18n 'admin.dashboard.problems_found'}} -

          - {{#each problems as |problem|}} -
        • {{{problem}}}
        • - {{/each}} -
        -

        -

        - {{i18n 'admin.dashboard.last_checked'}}: {{problemsTimestamp}} - {{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}} -

        - {{/conditional-loading-spinner}} -
        -
        -
        - {{else}} - {{#if thereWereProblems}} -
        -
         
        -
        -

        - {{i18n 'admin.dashboard.no_problems'}} - {{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}} -

        -
        -
        -
        - {{/if}} - {{/if}} -
        @@ -224,7 +182,7 @@ diff --git a/app/assets/javascripts/admin/templates/dashboard_next.hbs b/app/assets/javascripts/admin/templates/dashboard_next.hbs new file mode 100644 index 00000000000..1608d6238ce --- /dev/null +++ b/app/assets/javascripts/admin/templates/dashboard_next.hbs @@ -0,0 +1,175 @@ +{{plugin-outlet name="admin-dashboard-top"}} + +
        + {{#if showVersionChecks}} +
        + {{partial 'admin/templates/version-checks'}} + {{partial 'admin/templates/dashboard-problems'}} +
        + {{/if}} +
        +
        + +
        +
        +

        {{i18n "admin.dashboard.community_health"}}

        + {{period-chooser period=period action="changePeriod" content=availablePeriods fullDay=true}} +
        + +
        +
        + {{dashboard-mini-chart + dataSourceNames="signups" + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceNames="topics" + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceNames="posts" + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceNames="dau_by_mau" + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceNames="daily_engaged_users" + startDate=startDate + endDate=endDate}} + + {{dashboard-mini-chart + dataSourceNames="new_contributors" + startDate=startDate + endDate=endDate}} +
        +
        +
        + +
        +
        +
        + {{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}} +
        +

        {{i18n "admin.dashboard.activity_metrics"}}

        +
        + +
        +
        + + + + + + + + + + + {{#each reports as |report|}} + {{admin-report-counts report=report allTime=false}} + {{/each}} + +
        {{i18n 'admin.dashboard.reports.today'}}{{i18n 'admin.dashboard.reports.yesterday'}}{{i18n 'admin.dashboard.reports.last_7_days'}}{{i18n 'admin.dashboard.reports.last_30_days'}}
        +
        + {{/conditional-loading-section}} +
        +
        + {{dashboard-inline-table dataSourceNames="users_by_type" lastRefreshedAt=lastRefreshedAt}} + + {{dashboard-inline-table dataSourceNames="users_by_trust_level" lastRefreshedAt=lastRefreshedAt}} +
        + {{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}} +
        +
        + {{#if currentUser.admin}} +
        +

        + {{d-icon "archive"}} {{i18n "admin.dashboard.backups"}} +

        +

        + {{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}}) +
        + {{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}} +

        +
        + {{/if}} + +
        +

        {{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}

        +

        + {{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}}) +

        +
        +
        + +
        +
        +

        {{i18n "admin.dashboard.last_updated"}}

        +

        {{updatedTimestamp}}

        + + {{i18n "admin.dashboard.whats_new_in_discourse"}} + +
        +
        +
        + +

        + {{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}} +

        + {{/conditional-loading-section}} +
      + +
      +
      + {{#dashboard-table + dataSourceNames="top_referred_topics" + lastRefreshedAt=lastRefreshedAt + limit=8 + as |context|}} + {{#each context.report.data as |data|}} + + + + {{data.topic_title}} + + + + {{data.num_clicks}} + + + {{/each}} + {{/dashboard-table}} +
      + + +
      +
      diff --git a/app/assets/javascripts/admin/templates/email-index.hbs b/app/assets/javascripts/admin/templates/email-index.hbs index 7fc5ce5d838..80b3d5d062d 100644 --- a/app/assets/javascripts/admin/templates/email-index.hbs +++ b/app/assets/javascripts/admin/templates/email-index.hbs @@ -19,7 +19,7 @@
      {{text-field value=testEmailAddress placeholderKey="admin.email.test_email_address"}}
      -
      +
      {{#if sentTestEmail}}{{i18n 'admin.email.sent_test'}}{{/if}}
      diff --git a/app/assets/javascripts/admin/templates/emojis.hbs b/app/assets/javascripts/admin/templates/emojis.hbs index 675e8249be0..4cf568fd450 100644 --- a/app/assets/javascripts/admin/templates/emojis.hbs +++ b/app/assets/javascripts/admin/templates/emojis.hbs @@ -6,7 +6,7 @@

      {{emoji-uploader done="emojiUploaded"}}

      {{#if sortedEmojis}} -
      +
      diff --git a/app/assets/javascripts/admin/templates/flags-topics-index.hbs b/app/assets/javascripts/admin/templates/flags-topics-index.hbs index b5a7eca133e..5ece61b6b72 100644 --- a/app/assets/javascripts/admin/templates/flags-topics-index.hbs +++ b/app/assets/javascripts/admin/templates/flags-topics-index.hbs @@ -1,46 +1,50 @@ {{plugin-outlet name="flagged-topics-before" noTags=true args=(hash flaggedTopics=flaggedTopics)}} -
      - - {{plugin-outlet name="flagged-topic-header-row" noTags=true}} - - - - - - +{{#if flaggedTopics}} +
      {{i18n "admin.flags.flagged_topics.topic"}} {{i18n "admin.flags.flagged_topics.type"}}{{I18n "admin.flags.flagged_topics.users"}}{{i18n "admin.flags.flagged_topics.last_flagged"}}
      + + {{plugin-outlet name="flagged-topic-header-row" noTags=true}} + + + + + + - {{#each flaggedTopics as |ft|}} - - {{plugin-outlet name="flagged-topic-row" noTags=true args=(hash topic=ft.topic)}} + {{#each flaggedTopics as |ft|}} + + {{plugin-outlet name="flagged-topic-row" noTags=true args=(hash topic=ft.topic)}} - - - - - - - {{/each}} -
      {{i18n "admin.flags.flagged_topics.topic"}} {{i18n "admin.flags.flagged_topics.type"}}{{I18n "admin.flags.flagged_topics.users"}}{{i18n "admin.flags.flagged_topics.last_flagged"}}
      - {{replace-emoji ft.topic.fancy_title}} - - {{#each ft.flag_counts as |fc|}} -
      - {{post-action-title fc.post_action_type_id fc.name_key}} - x{{fc.count}} -
      - {{/each}} -
      - {{flagged-topic-users users=ft.users tagName=""}} - - {{format-age ft.last_flag_at}} - - {{#link-to - "adminFlags.topics.show" - ft.id - class="btn d-button no-text btn-small btn-primary show-details" - title=(i18n "admin.flags.show_details")}} - {{d-icon "list"}} - {{i18n "admin.flags.details"}} - {{/link-to}} -
      + + {{replace-emoji ft.topic.fancy_title}} + + + {{#each ft.flag_counts as |fc|}} +
      + {{post-action-title fc.post_action_type_id fc.name_key}} + x{{fc.count}} +
      + {{/each}} + + + {{flagged-topic-users users=ft.users tagName=""}} + + + {{format-age ft.last_flag_at}} + + + {{#link-to + "adminFlags.topics.show" + ft.id + class="btn d-button no-text btn-small btn-primary show-details" + title=(i18n "admin.flags.show_details")}} + {{d-icon "list"}} + {{i18n "admin.flags.details"}} + {{/link-to}} + + + {{/each}} + +{{else}} + {{i18n "admin.flags.flagged_topics.no_results"}} +{{/if}} diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs deleted file mode 100644 index 0689bd30ce1..00000000000 --- a/app/assets/javascripts/admin/templates/group.hbs +++ /dev/null @@ -1,173 +0,0 @@ - - -
      - {{#if model.automatic}} -

      {{model.name}}

      - {{else}} - - {{text-field name="name" value=model.name placeholderKey="groups.name_placeholder"}} - {{/if}} -
      - - {{#unless model.automatic}} -
      - - {{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}} -
      - -
      - - {{d-editor value=model.bio_raw}} -
      - - {{#if model.hasOwners}} -
      - -
      - {{#each model.owners as |member|}} - {{group-member member=member removeAction="removeOwner"}} - {{/each}} -
      -
      - {{/if}} - -
      - - - {{user-selector usernames=model.ownerUsernames - placeholderKey="admin.groups.selector_placeholder" - id="owner-selector"}} - - {{#if model.id}} - {{d-button - action="addOwners" - class="add" - icon="plus" - label="admin.groups.add"}} - {{/if}} -
      - {{/unless}} - -
      - {{group-members-input model=model addButton=model.id}} -
      - -
      - - {{combo-box name="alias" - valueAttribute="value" - value=model.visibility_level - content=visibilityLevelOptions - castInteger=true}} -
      - - {{#unless model.automatic}} -
      - -
      - -
      - -
      - -
      - -
      - - {{#if model.allow_membership_requests}} -
      - - - {{expanding-text-area name="membership-request-template" - value=model.membership_request_template}} -
      - {{/if}} - -
      - -
      - {{/unless}} - -
      - - {{combo-box name="alias" valueAttribute="value" value=model.mentionable_level content=aliasLevelOptions}} -
      - -
      - - {{combo-box name="alias" valueAttribute="value" value=model.messageable_level content=aliasLevelOptions}} -
      - -
      - - {{notifications-button i18nPrefix='groups.notifications' value=model.default_notification_level}} -
      -
      - - {{#unless model.automatic}} -
      - - {{list-setting name="automatic_membership" settingValue=model.emailDomains}} - -
      - -
      - - {{input value=model.title}} -
      - -
      - - {{combo-box name="grant_trust_level" valueAttribute="value" value=model.grant_trust_level content=trustLevelOptions}} -
      - - {{#if siteSettings.email_in}} - - {{text-field name="incoming_email" value=model.incoming_email placeholderKey="admin.groups.incoming_email_placeholder"}} - {{plugin-outlet name="group-email-in" args=(hash model=model)}} - {{/if}} - {{/unless}} - - {{#unless model.automatic}} - {{group-flair-inputs model=model}} - {{/unless}} - - {{plugin-outlet name="group-edit" args=(hash group=model)}} - -
      - - {{#unless model.automatic}} - - {{/unless}} - - {{savingStatus}} -
      - - diff --git a/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs b/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs deleted file mode 100644 index 6a2d8b13e5b..00000000000 --- a/app/assets/javascripts/admin/templates/groups-bulk-complete.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{#if bulkAddResponse}} -

      {{{bulkAddResponse.message}}}

      - {{#if bulkAddResponse.users_not_added}} -

      {{i18n "admin.groups.bulk_complete_users_not_added"}}

      - {{#each bulkAddResponse.users_not_added as |user|}} - {{user}}
      - {{/each}} - {{/if}} -{{else}} -

      {{i18n "admin.groups.bulk_complete"}}

      -{{/if}} diff --git a/app/assets/javascripts/admin/templates/groups-bulk.hbs b/app/assets/javascripts/admin/templates/groups-bulk.hbs deleted file mode 100644 index 337ab37cb3e..00000000000 --- a/app/assets/javascripts/admin/templates/groups-bulk.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
      -

      {{i18n "admin.groups.bulk_paste"}}

      - -
      - {{textarea value=users class="paste-users"}} -
      - -
      - {{combo-box filterable=true content=groups value=groupId none="admin.groups.bulk_select"}} -
      - -
      - {{d-button disabled=buttonDisabled - class="btn-primary" - action="addToGroup" - icon="plus" - label="admin.groups.bulk"}} -
      -
      diff --git a/app/assets/javascripts/admin/templates/groups-type-index.hbs b/app/assets/javascripts/admin/templates/groups-type-index.hbs deleted file mode 100644 index 196ded96d2e..00000000000 --- a/app/assets/javascripts/admin/templates/groups-type-index.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
      -

      {{i18n messageKey}}

      - -
      - {{#link-to 'adminGroup' 'new' class="btn"}} - {{d-icon "plus"}} {{i18n 'admin.groups.new'}} - {{/link-to}} -
      -
      diff --git a/app/assets/javascripts/admin/templates/groups-type.hbs b/app/assets/javascripts/admin/templates/groups-type.hbs deleted file mode 100644 index a1fa8298970..00000000000 --- a/app/assets/javascripts/admin/templates/groups-type.hbs +++ /dev/null @@ -1,31 +0,0 @@ -
      - {{#if sortedGroups}} -
      -

      {{i18n 'admin.groups.edit'}}

      -
        - {{#each sortedGroups as |group|}} -
      • - {{#link-to "adminGroup" group.type group.name}}{{group.name}} - {{#if group.userCountDisplay}} - {{number group.userCountDisplay}} - {{/if}} - {{/link-to}} -
      • - {{/each}} -
      -
      - {{#if isAuto}} - {{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}} - {{else}} - {{#link-to 'adminGroup' 'new' class="btn"}} - {{d-icon "plus"}} {{i18n 'admin.groups.new'}} - {{/link-to}} - {{/if}} -
      -
      - {{/if}} - -
      - {{outlet}} -
      -
      diff --git a/app/assets/javascripts/admin/templates/groups.hbs b/app/assets/javascripts/admin/templates/groups.hbs deleted file mode 100644 index aa7d9213ca8..00000000000 --- a/app/assets/javascripts/admin/templates/groups.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#admin-nav}} - {{nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}} - {{nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}} - {{nav-item route='adminGroups.bulk' label='admin.groups.bulk'}} -{{/admin-nav}} - -
      - {{outlet}} -
      diff --git a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs index 00e67a9aa5c..e8ee4d896dd 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs @@ -58,7 +58,7 @@ {{#unless item.editing}} {{d-button action="destroy" actionParam=item icon="trash-o" class="btn-danger"}} {{d-button action="edit" actionParam=item icon="pencil"}} - {{#if isBlocked}} + {{#if item.isBlocked}} {{d-button action="allow" actionParam=item icon="check" label="admin.logs.screened_ips.actions.do_nothing"}} {{else}} {{d-button action="block" actionParam=item icon="ban" label="admin.logs.screened_ips.actions.block"}} diff --git a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs index 5df150c79fb..3445abf8df5 100644 --- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs @@ -39,7 +39,7 @@
      -
      +{{#staff-actions}}
      {{i18n 'admin.logs.staff_actions.staff_user'}}
      {{i18n 'admin.logs.action'}}
      @@ -86,4 +86,4 @@ {{i18n 'search.no_results'}} {{/each}} {{/conditional-loading-spinner}} -
      +{{/staff-actions}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-add-upload.hbs b/app/assets/javascripts/admin/templates/modal/admin-add-upload.hbs index 2bd1691a325..18065237a4f 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-add-upload.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-add-upload.hbs @@ -5,6 +5,11 @@ + {{#if fileSelected}} + {{#if errorMessage}} + {{errorMessage}} + {{/if}} + {{/if}}
      {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-flags-received.hbs b/app/assets/javascripts/admin/templates/modal/admin-flags-received.hbs new file mode 100644 index 00000000000..9f05993cf90 --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-flags-received.hbs @@ -0,0 +1,14 @@ +{{#d-modal-body rawTitle=(i18n "admin.user.flags_received_by" username=model.username)}} + {{#conditional-loading-spinner condition=loadingFlags}} + {{#each flaggedPosts as |flaggedPost|}} +
      +
      + {{flagged-post-title flaggedPost=flaggedPost}} +
      + {{flag-user-lists flaggedPost=flaggedPost showResolvedBy=flaggedPost.hasDisposedBy}} +
      + {{else}} + {{i18n "admin.user.flags_received_none"}} + {{/each}} + {{/conditional-loading-spinner}} +{{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs index 19d7065db61..c00f67c941e 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs @@ -4,7 +4,7 @@ {{#if local}}
      -
      +
      {{i18n 'admin.customize.theme.import_file_tip'}}
      {{/if}} @@ -16,7 +16,24 @@
      {{input value=uploadUrl placeholder="https://github.com/discourse/discourse/sample_theme"}} {{i18n 'admin.customize.theme.import_web_tip'}} + {{#if checkPrivate}} +
      + + {{#if privateChecked}} + {{#if publicKey}} +
      + {{i18n 'admin.customize.theme.public_key'}} + {{textarea disabled=true value=publicKey}} +
      + {{/if}} + {{/if}} +
      + {{/if}}
      + {{/if}}
      {{/d-modal-body}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs index 0abe77544c6..b31251a91d5 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs @@ -11,28 +11,13 @@
      -
      - - {{text-field - value=reason - class="silence-reason" - placeholderKey="admin.user.silence_reason_placeholder"}} -
      - - - {{textarea - value=message - class="silence-message" - placeholder=(i18n "admin.user.silence_message_placeholder")}} - + {{silence-details reason=reason message=message}} + {{#if post}} + {{penalty-post-action + post=post + postAction=postAction + postEdit=postEdit}} + {{/if}} {{/conditional-loading-spinner}} @@ -47,4 +32,4 @@ label="admin.user.silence"}} {{d-modal-cancel close=(action "closeModal")}} {{conditional-loading-spinner condition=loading size="small"}} -
      \ No newline at end of file +
    diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs index fc6025a6a63..6c2e8e1636a 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs @@ -11,32 +11,15 @@ input=suspendUntil}}
    + {{suspension-details reason=reason message=message}} -
    - - {{text-field - value=reason - class="suspend-reason" - placeholderKey="admin.user.suspend_reason_placeholder"}} -
    + {{#if post}} + {{penalty-post-action + post=post + postAction=postAction + postEdit=postEdit}} + {{/if}} - - {{textarea - value=message - class="suspend-message" - placeholder=(i18n "admin.user.suspend_message_placeholder")}} {{else}}
    {{i18n "admin.user.cant_suspend"}} diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs index b7d64f2a346..dc5405471ba 100644 --- a/app/assets/javascripts/admin/templates/plugins-index.hbs +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -14,6 +14,7 @@ + @@ -23,6 +24,14 @@ {{#each model as |plugin|}} + + + + + + + + + + + + + +
    {{i18n "admin.plugins.name"}} {{i18n "admin.plugins.version"}} {{i18n "admin.plugins.enabled"}}
    + {{#if plugin.is_official}} + {{d-icon "check-circle" + title="admin.plugins.official" + class="admin-plugins-official-badge"}} + {{/if}} + {{#if plugin.url}} {{plugin.name}} @@ -58,4 +67,3 @@ {{/if}}

    {{i18n "admin.plugins.howto"}}

    - diff --git a/app/assets/javascripts/admin/templates/reports.hbs b/app/assets/javascripts/admin/templates/reports.hbs index 3927d1e384f..26b5bfb4fe6 100644 --- a/app/assets/javascripts/admin/templates/reports.hbs +++ b/app/assets/javascripts/admin/templates/reports.hbs @@ -1,50 +1,52 @@

    {{model.title}}

    -
    - {{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}} - {{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}} - {{#if showCategoryOptions}} - {{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}} - {{/if}} - {{#if showGroupOptions}} - {{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}} - {{/if}} - {{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}} - {{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}} +{{#if model.description}} +

    {{model.description}}

    +{{/if}} + +
    +
    + {{#conditional-loading-spinner condition=refreshing}} +
    + {{#if viewingTable}} + {{i18n 'admin.dashboard.reports.view_table'}} + {{else}} + {{i18n 'admin.dashboard.reports.view_table'}} + {{/if}} + | + {{#if viewingGraph}} + {{i18n 'admin.dashboard.reports.view_graph'}} + {{else}} + {{i18n 'admin.dashboard.reports.view_graph'}} + {{/if}} +
    + + {{#if viewingGraph}} + {{admin-graph model=model}} + {{else}} + {{admin-table-report model=model}} + {{/if}} + + {{#if model.relatedReport}} + {{admin-table-report model=model.relatedReport}} + {{/if}} + {{/conditional-loading-spinner}} +
    + +
    + + {{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}} + + + {{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}} + + {{#if showCategoryOptions}} + {{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}} + {{/if}} + {{#if showGroupOptions}} + {{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}} + {{/if}} + {{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}} + {{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}} +
    - -
    - {{#if viewingTable}} - {{i18n 'admin.dashboard.reports.view_table'}} - {{else}} - {{i18n 'admin.dashboard.reports.view_table'}} - {{/if}} - | - {{#if viewingGraph}} - {{i18n 'admin.dashboard.reports.view_graph'}} - {{else}} - {{i18n 'admin.dashboard.reports.view_graph'}} - {{/if}} -
    - -{{#conditional-loading-spinner condition=refreshing}} - {{#if viewingGraph}} - {{admin-graph model=model}} - {{else}} - - - - - - - {{#each model.dataReversed as |row|}} - - - - - {{/each}} -
    {{model.xaxis}}{{model.yaxis}}
    {{row.x}} - {{row.y}} -
    - {{/if}} -{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/search-logs-term.hbs b/app/assets/javascripts/admin/templates/search-logs-term.hbs index c405a86d358..5e640039c34 100644 --- a/app/assets/javascripts/admin/templates/search-logs-term.hbs +++ b/app/assets/javascripts/admin/templates/search-logs-term.hbs @@ -10,4 +10,53 @@ {{#conditional-loading-spinner condition=refreshing}} {{admin-graph model=model}} + +

    +

    {{i18n "admin.logs.search_logs.header_search_results"}}

    +
    + +
    + {{#each model.search_result.posts as |result|}} +
    + + +
    +
    + + {{topic-status topic=result.topic disableActions=true}}{{#highlight-text highlight=term}}{{{unbound result.topic.fancyTitle}}}{{/highlight-text}} + + +
    + {{#if result.topic.category.parentCategory}} + {{category-link result.topic.category.parentCategory}} + {{/if}} + {{category-link result.topic.category hideParent=true}} + {{#each result.topic.tags as |tag|}} + {{discourse-tag tag}} + {{/each}} +
    +
    + +
    + + {{format-age result.created_at}} + {{#if result.blurb}} + - + {{/if}} + + + {{#if result.blurb}} + {{#highlight-text highlight=term}} + {{{unbound result.blurb}}} + {{/highlight-text}} + {{/if}} +
    +
    +
    + {{/each}} +
    {{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/admin/templates/site-settings-category.hbs b/app/assets/javascripts/admin/templates/site-settings-category.hbs index 2e44f80c38c..5f612419326 100644 --- a/app/assets/javascripts/admin/templates/site-settings-category.hbs +++ b/app/assets/javascripts/admin/templates/site-settings-category.hbs @@ -1,8 +1,11 @@ {{#if filteredContent}} {{#d-section class="form-horizontal settings"}} {{#each filteredContent as |setting|}} - {{site-setting setting=setting saveAction="saveSetting"}} + {{site-setting setting=setting}} {{/each}} + {{#if category.hasMore}} +

    {{i18n 'admin.site_settings.more_than_30_results'}}

    + {{/if}} {{/d-section}} {{else}}
    diff --git a/app/assets/javascripts/admin/templates/site-settings.hbs b/app/assets/javascripts/admin/templates/site-settings.hbs index 4561129eb86..9f455b01e92 100644 --- a/app/assets/javascripts/admin/templates/site-settings.hbs +++ b/app/assets/javascripts/admin/templates/site-settings.hbs @@ -2,7 +2,7 @@
    @@ -18,9 +18,7 @@
  • {{#link-to 'adminSiteSettingsCategory' category.nameKey class=category.nameKey}} {{category.name}} - {{#if filtered}} - {{#if category.count}}({{category.count}}){{/if}} - {{/if}} + {{#if category.count}}({{category.count}}){{/if}} {{/link-to}}
  • {{/each}} diff --git a/app/assets/javascripts/admin/templates/user-badges.hbs b/app/assets/javascripts/admin/templates/user-badges.hbs index 1f6bae617c9..52faf9b4333 100644 --- a/app/assets/javascripts/admin/templates/user-badges.hbs +++ b/app/assets/javascripts/admin/templates/user-badges.hbs @@ -1,16 +1,16 @@
    -
    +
    +
    {{#conditional-loading-spinner condition=loading}}

    {{i18n 'admin.badges.grant_badge'}}


    - {{#if noBadges}} + {{#if noGrantableBadges}}

    {{i18n 'admin.badges.no_badges'}}

    {{else}}
    @@ -22,7 +22,7 @@ {{input type="text" value=badgeReason}}
    {{i18n 'admin.badges.reason_help'}} - +
    {{/if}} diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 96f962a0b50..e36f7cd4b5c 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -6,13 +6,11 @@ {{i18n 'admin.user.show_public_profile'}} {{/link-to}} {{/if}} + {{#if model.can_view_action_logs}} {{d-button action="viewActionLogs" actionParam=model.username icon="list-alt" label="admin.user.action_logs"}} {{/if}} {{#if model.active}} - {{#if model.can_impersonate}} - {{d-button class="btn-danger" action="impersonate" icon="crosshairs" label="admin.impersonate.title" title="admin.impersonate.help"}} - {{/if}} {{#if currentUser.admin}} {{d-button action="logOut" icon="power-off" label="admin.user.log_out"}} {{/if}} @@ -58,6 +56,8 @@
    + {{plugin-outlet name="admin-user-below-names" args=(hash user=model) tagName='' connectorTagName=''}} + {{#if canCheckEmails}}
    -
    +
    {{i18n 'user.ip_address.title'}}
    {{model.ip_address}}
    @@ -134,7 +134,7 @@
    -
    +
    {{i18n 'user.registration_ip_address.title'}}
    {{model.registration_ip_address}}
    @@ -155,6 +155,22 @@
    {{/if}} + +
    +
    {{i18n 'user.second_factor.title'}}
    +
    + {{#if model.second_factor_enabled}} + {{i18n "yes_value"}} + {{else}} + {{i18n "no_value"}} + {{/if}} +
    +
    + {{#if canDisableSecondFactor}} + {{d-button action="disableSecondFactor" icon="unlock-alt" label="user.second_factor.disable"}} + {{/if}} +
    +
    {{#if userFields}} @@ -185,8 +201,8 @@
    {{#if model.approved}} {{i18n 'admin.user.approved_by'}} - {{#link-to 'adminUser' approvedBy}}{{avatar model.approvedBy imageSize="small"}}{{/link-to}} - {{#link-to 'adminUser' approvedBy}}{{model.approvedBy.username}}{{/link-to}} + {{#link-to 'adminUser' model.approvedBy}}{{avatar model.approvedBy imageSize="small"}}{{/link-to}} + {{#link-to 'adminUser' model.approvedBy}}{{model.approvedBy.username}}{{/link-to}} {{else}} {{i18n 'no_value'}} {{/if}} @@ -302,18 +318,18 @@
    -
    -
    {{i18n 'admin.user.suspended'}}
    -
    - {{i18n-yes-no model.isSuspended}} - {{#if model.isSuspended}} - {{#unless model.suspendedForever}} - {{i18n "admin.user.suspended_until" until=model.suspendedTillDate}} - {{/unless}} - {{/if}} -
    -
    - {{#if model.isSuspended}} +
    +
    {{i18n 'admin.user.suspended'}}
    +
    + {{i18n-yes-no model.suspended}} + {{#if model.suspended}} + {{#unless model.suspendedForever}} + {{i18n "admin.user.suspended_until" until=model.suspendedTillDate}} + {{/unless}} + {{/if}} +
    +
    + {{#if model.suspended}} {{d-button class="btn-danger unsuspend-user" action=(action "unsuspend") @@ -333,12 +349,12 @@
    - {{#if model.isSuspended}} + {{#if model.suspended}}
    {{i18n 'admin.user.suspended_by'}}
    - {{#link-to 'adminUser' suspendedBy}}{{avatar model.suspendedBy imageSize="tiny"}}{{/link-to}} - {{#link-to 'adminUser' suspendedBy}}{{model.suspendedBy.username}}{{/link-to}} + {{#link-to 'adminUser' model.suspendedBy}}{{avatar model.suspendedBy imageSize="tiny"}}{{/link-to}} + {{#link-to 'adminUser' model.suspendedBy}}{{model.suspendedBy.username}}{{/link-to}}
    {{i18n 'admin.user.suspend_reason'}}: @@ -351,7 +367,7 @@
    {{i18n 'admin.user.silenced'}}
    {{i18n-yes-no model.silenced}} - {{#if model.isSilenced}} + {{#if model.silenced}} {{#unless model.silencedForever}} {{i18n "admin.user.suspended_until" until=model.silencedTillDate}} {{/unless}} @@ -378,12 +394,12 @@
    - {{#if model.isSilenced}} + {{#if model.silenced}}
    {{i18n 'admin.user.silenced_by'}}
    - {{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}} - {{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}} + {{#link-to 'adminUser' model.silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}} + {{#link-to 'adminUser' model.silencedBy}}{{model.silencedBy.username}}{{/link-to}}
    {{i18n 'admin.user.silence_reason'}}: @@ -392,6 +408,21 @@
    {{/if}} + {{#if model.tl3_requirements.penalty_counts.total}} +
    +
    {{i18n 'admin.user.penalty_count'}}
    +
    {{model.tl3_requirements.penalty_counts.total}}
    + {{#if currentUser.admin}} +
    + {{d-button label="admin.user.clear_penalty_history.title" + icon="times" + action=(action "clearPenaltyHistory")}} + {{i18n "admin.user.clear_penalty_history.description"}} +
    + {{/if}} +
    + {{/if}} + {{#if currentUser.admin}} @@ -399,7 +430,7 @@

    {{i18n 'admin.groups.title'}}

    {{i18n 'admin.groups.automatic'}}
    -
    {{automaticGroups}}
    +
    {{{automaticGroups}}}
    {{i18n 'admin.groups.custom'}}
    @@ -452,7 +483,7 @@ {{d-button class="btn-danger" action="deleteAllPosts" icon="trash-o" label="admin.user.delete_all_posts"}} {{/if}} {{else}} - {{model.deleteAllPostsExplanation}} + {{deleteAllPostsExplanation}} {{/if}}
    @@ -466,7 +497,18 @@
    {{i18n 'admin.user.flags_given_received_count'}}
    -
    {{model.flags_given_count}} / {{model.flags_received_count}}
    +
    + {{model.flags_given_count}} / {{model.flags_received_count}} +
    +
    + {{#if model.flags_received_count}} + {{d-button + action=(action "showFlagsReceived") + label="admin.user.show_flags_received" + icon="flag" + }} + {{/if}} +
    {{i18n 'admin.user.private_topics_count'}}
    @@ -511,31 +553,37 @@ {{/if}} +{{plugin-outlet name="after-user-details" args=(hash model=model)}} +

    - {{#unless model.anonymizeForbidden}} + {{#if model.active}} + {{#if model.can_impersonate}} + {{d-button class="btn-danger" action="impersonate" icon="crosshairs" label="admin.impersonate.title" title="admin.impersonate.help"}} + {{/if}} + {{/if}} + + {{#if model.can_be_anonymized}} {{d-button label="admin.user.anonymize" icon="exclamation-triangle" class="btn-danger" - disabled=model.anonymizeForbidden action="anonymize"}} - {{/unless}} + {{/if}} - {{#unless model.deleteForbidden}} + {{#if model.canBeDeleted}} {{d-button label="admin.user.delete" icon="exclamation-triangle" class="btn-danger" - disabled=model.deleteForbidden action="destroy"}} - {{/unless}} + {{/if}}
    - {{#if model.deleteExplanation}} + {{#if deleteExplanation}}

    - {{d-icon "exclamation-triangle"}} {{model.deleteExplanation}} + {{d-icon "exclamation-triangle"}} {{deleteExplanation}}
    {{/if}}
    diff --git a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs index 1561a95670b..1ac192e1926 100644 --- a/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs +++ b/app/assets/javascripts/admin/templates/user-tl3-requirements.hbs @@ -1,10 +1,10 @@
    -
    +
    +
    @@ -96,6 +96,18 @@
    {{model.tl3Requirements.num_likes_received_users}} {{model.tl3Requirements.min_likes_received_users}}
    {{i18n 'admin.user.tl3_requirements.silenced'}}{{check-icon model.tl3Requirements.met.silenced}}{{model.tl3Requirements.penalty_counts.silenced}}0
    {{i18n 'admin.user.tl3_requirements.suspended'}}{{check-icon model.tl3Requirements.met.suspended}}{{model.tl3Requirements.penalty_counts.suspended}}0
    diff --git a/app/assets/javascripts/admin/templates/users-list-show.hbs b/app/assets/javascripts/admin/templates/users-list-show.hbs index 0ccaee02513..7401077ee63 100644 --- a/app/assets/javascripts/admin/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/templates/users-list-show.hbs @@ -98,6 +98,10 @@ {{#if user.moderator}} {{d-icon "shield" title="admin.moderator" }} {{/if}} + + {{#if user.second_factor_enabled}} + {{d-icon "lock" title="admin.user.second_factor_enabled" }} + {{/if}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/users-list.hbs b/app/assets/javascripts/admin/templates/users-list.hbs index 398732685b1..c101a1a13bd 100644 --- a/app/assets/javascripts/admin/templates/users-list.hbs +++ b/app/assets/javascripts/admin/templates/users-list.hbs @@ -1,5 +1,5 @@
    -
    +
    +
    {{#unless siteSettings.enable_sso}} {{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}} diff --git a/app/assets/javascripts/admin/templates/version-checks.hbs b/app/assets/javascripts/admin/templates/version-checks.hbs index a8724b7e5a5..abd5f4befdd 100644 --- a/app/assets/javascripts/admin/templates/version-checks.hbs +++ b/app/assets/javascripts/admin/templates/version-checks.hbs @@ -1,73 +1,83 @@ -
    - - {{custom-html name="upgrade-header" versionCheck=versionCheck tagName="thead"}} -
    +
    +

    {{i18n 'admin.dashboard.version'}}

    +
    +
    - - - - - - - - - - - +
    +

    {{i18n 'admin.dashboard.installed_version'}}

    +

    {{dash-if-empty versionCheck.installed_describe}}

    +
    {{#if versionCheck.noCheckPerformed}} - - - +
    +

    {{i18n 'admin.dashboard.latest_version'}}

    +

    +
    +
    +
    + {{d-icon "frown-o"}} +
    +
    + {{i18n 'admin.dashboard.no_check_performed'}} +
    +
    {{else}} {{#if versionCheck.stale_data}} - - - - {{else}} - - - + + + {{else}} +
    +

    {{i18n 'admin.dashboard.latest_version'}}

    +

    {{dash-if-empty versionCheck.latest_version}}

    +
    +
    +
    + {{#if versionCheck.upToDate }} + {{d-icon "smile-o"}} + {{else}} + + {{#if versionCheck.behindByOneVersion}} + {{d-icon "meh-o"}} + {{else}} + {{d-icon "frown-o"}} + {{/if}} + + {{/if}} +
    +
    + {{#if versionCheck.upToDate }} + {{i18n 'admin.dashboard.up_to_date'}} + {{else}} + {{i18n 'admin.dashboard.critical_available'}} + {{i18n 'admin.dashboard.updates_available'}} + {{i18n 'admin.dashboard.please_upgrade'}} + {{/if}} +
    +
    + {{/if}} {{/if}} - -
     {{i18n 'admin.dashboard.installed_version'}}{{i18n 'admin.dashboard.latest_version'}}
    {{i18n 'admin.dashboard.version'}}{{dash-if-empty versionCheck.installed_describe}} - {{d-icon "frown-o"}} - - {{i18n 'admin.dashboard.no_check_performed'}} - {{#if versionCheck.version_check_pending}}{{dash-if-empty versionCheck.installed_version}}{{/if}} - {{#if versionCheck.version_check_pending}} - {{d-icon "smile-o"}} - {{else}} - {{d-icon "frown-o"}} - {{/if}} - - +
    +

    {{i18n 'admin.dashboard.latest_version'}}

    +

    {{#if versionCheck.version_check_pending}}{{dash-if-empty versionCheck.installed_version}}{{/if}}

    +
    +
    +
    {{#if versionCheck.version_check_pending}} - {{i18n 'admin.dashboard.version_check_pending'}} + {{d-icon "smile-o"}} {{else}} - {{i18n 'admin.dashboard.stale_data'}} + {{d-icon "frown-o"}} {{/if}} - -
    {{dash-if-empty versionCheck.latest_version}} - {{#if versionCheck.upToDate }} - {{d-icon "smile-o"}} - {{else}} - - {{#if versionCheck.behindByOneVersion}} - {{d-icon "meh-o"}} + +
    + + {{#if versionCheck.version_check_pending}} + {{i18n 'admin.dashboard.version_check_pending'}} {{else}} - {{d-icon "frown-o"}} + {{i18n 'admin.dashboard.stale_data'}} {{/if}} - {{/if}} -
    - {{#if versionCheck.upToDate }} - {{i18n 'admin.dashboard.up_to_date'}} - {{else}} - {{i18n 'admin.dashboard.critical_available'}} - {{i18n 'admin.dashboard.updates_available'}} - {{i18n 'admin.dashboard.please_upgrade'}} - {{/if}} -
    + + {{custom-html name="upgrade-header" versionCheck=versionCheck tagName="div" classNames="upgrade-header"}} +
    diff --git a/app/assets/javascripts/admin/templates/watched-words-action.hbs b/app/assets/javascripts/admin/templates/watched-words-action.hbs index 8377c28bd50..1cf0f77cd92 100644 --- a/app/assets/javascripts/admin/templates/watched-words-action.hbs +++ b/app/assets/javascripts/admin/templates/watched-words-action.hbs @@ -5,17 +5,24 @@ {{watched-word-form actionKey=actionNameKey action="recordAdded" + filteredContent=filteredContent regularExpressions=adminWatchedWords.regularExpressions}} {{watched-word-uploader uploading=uploading actionKey=actionNameKey done="uploadComplete"}}
    +
    + +
    {{#if showWordsList}} {{#each filteredContent as |word| }}
    {{admin-watched-word word=word action="recordRemoved"}}
    {{/each}} {{else}} - {{i18n 'admin.watched_words.word_count' count=model.words.length}} + {{i18n 'admin.watched_words.word_count' count=wordCount}} {{/if}}
    diff --git a/app/assets/javascripts/admin/templates/watched-words.hbs b/app/assets/javascripts/admin/templates/watched-words.hbs index 7bec3d88120..cf5f50cc643 100644 --- a/app/assets/javascripts/admin/templates/watched-words.hbs +++ b/app/assets/javascripts/admin/templates/watched-words.hbs @@ -1,11 +1,6 @@
    -
    + {{d-button action="toggleMenu" class="menu-toggle" icon="bars"}} {{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}} {{d-button action="clearFilter" label="admin.watched_words.clear_filter"}}
    diff --git a/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs b/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs index e0d06098713..d2f926a9424 100644 --- a/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs +++ b/app/assets/javascripts/admin/templates/web-hooks-show-events.hbs @@ -23,7 +23,6 @@ {{#if hasIncoming}}
    {{count-i18n key="admin.web_hooks.events.incoming" count=incomingCount}} - {{i18n 'click_to_show'}}
    {{/if}}
      diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6b0d64be848..e7e9db37b4c 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -9,8 +9,11 @@ //= require ./deprecated // Stuff we need to load first +//= require ./discourse/helpers/parse-html +//= require ./discourse/lib/to-markdown //= require ./discourse/lib/utilities //= require ./discourse/lib/page-visible +//= require ./discourse/lib/logout //= require ./discourse/lib/ajax //= require ./discourse/lib/text //= require ./discourse/lib/hash @@ -25,6 +28,7 @@ //= require ./discourse/lib/key-value-store //= require ./discourse/lib/computed //= require ./discourse/lib/formatter +//= require ./discourse/lib/text-direction //= require ./discourse/lib/eyeline //= require ./discourse/lib/show-modal //= require ./discourse/mixins/scrolling @@ -34,6 +38,8 @@ //= require ./discourse/models/result-set //= require ./discourse/models/store //= require ./discourse/models/action-summary +//= require ./discourse/models/permission-type +//= require ./discourse/models/category //= require ./discourse/models/topic //= require ./discourse/models/draft //= require ./discourse/models/composer @@ -41,7 +47,6 @@ //= require ./discourse/models/badge //= require ./discourse/models/permission-type //= require ./discourse/models/user-action-group -//= require ./discourse/models/category //= require ./discourse/models/input-validation //= require ./discourse/lib/search //= require ./discourse/lib/user-search @@ -60,9 +65,10 @@ //= require ./discourse/models/user-action //= require ./discourse/models/draft //= require ./discourse/models/composer +//= require ./discourse/models/user-badge +//= require_tree ./discourse/lib //= require_tree ./discourse/mixins //= require ./discourse/models/invite -//= require ./discourse/models/user-badge //= require ./discourse/controllers/discovery-sortable //= require ./discourse/controllers/navigation/default //= require ./discourse/components/edit-category-panel @@ -84,7 +90,6 @@ //= require ./discourse/helpers/loading-spinner //= require ./discourse/helpers/category-link //= require ./discourse/lib/export-result -//= require_tree ./discourse/lib //= require ./discourse/mapping-router //= require_tree ./discourse/controllers diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 84242d03f65..1a6c453f414 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -9,7 +9,7 @@ const REPLACEMENTS = { 'd-watching-first': 'dot-circle-o', 'd-drop-expanded': 'caret-down', 'd-drop-collapsed': 'caret-right', - 'd-unliked': 'heart', + 'd-unliked': 'heart-o', 'd-liked': 'heart', 'notification.mentioned': "at", 'notification.group_mentioned': "at", @@ -90,7 +90,7 @@ registerIconRenderer({ if (params.label) { html += " aria-hidden='true'"; } html += `>`; if (params.label) { - html += "" + I18n.t(params.label) + ""; + html += `${params.label}`; } return html; }, diff --git a/app/assets/javascripts/discourse-common/lib/raw-handlebars.js.es6 b/app/assets/javascripts/discourse-common/lib/raw-handlebars.js.es6 index 4b547a68828..2900e5d6704 100644 --- a/app/assets/javascripts/discourse-common/lib/raw-handlebars.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/raw-handlebars.js.es6 @@ -18,13 +18,15 @@ RawHandlebars.helpers['get'] = function(context, options) { var firstContext = options.contexts[0]; var val = firstContext[context]; - if (context.indexOf('controller') === 0) { - context = context.replace(/^controller\./, ''); + if (context.indexOf('controller.') === 0) { + context = context.slice(context.indexOf('.') + 1); } - if (val && val.isDescriptor) { return Em.get(firstContext, context); } - val = val === undefined ? Em.get(firstContext, context): val; - return val; + if (val && val.isDescriptor) { + return Em.get(firstContext, context); + } + + return val === undefined ? Em.get(firstContext, context) : val; }; // adds compatability so this works with stringParams diff --git a/app/assets/javascripts/discourse-common/resolver.js.es6 b/app/assets/javascripts/discourse-common/resolver.js.es6 index 4c2dec2f0bf..569fb3a0a24 100644 --- a/app/assets/javascripts/discourse-common/resolver.js.es6 +++ b/app/assets/javascripts/discourse-common/resolver.js.es6 @@ -195,7 +195,8 @@ export function buildResolver(baseName) { findAdminTemplate(parsedName) { var decamelized = parsedName.fullNameWithoutType.decamelize(); if (decamelized.indexOf('components') === 0) { - const compTemplate = Ember.TEMPLATES['admin/templates/' + decamelized]; + let comPath = `admin/templates/${decamelized}`; + const compTemplate = Ember.TEMPLATES[`javascripts/${comPath}`] || Ember.TEMPLATES[comPath]; if (compTemplate) { return compTemplate; } } diff --git a/app/assets/javascripts/discourse/adapters/group.js.es6 b/app/assets/javascripts/discourse/adapters/group.js.es6 new file mode 100644 index 00000000000..34134b50ea3 --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/group.js.es6 @@ -0,0 +1,7 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + appendQueryParams(path, findArgs) { + return this._super(path, findArgs, '.json'); + }, +}); diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index c826bbefc9c..e33a755970b 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -40,7 +40,7 @@ export default Ember.Object.extend({ return "/"; }, - appendQueryParams(path, findArgs) { + appendQueryParams(path, findArgs, extension) { if (findArgs) { if (typeof findArgs === "object") { const queryString = Object.keys(findArgs) @@ -48,11 +48,11 @@ export default Ember.Object.extend({ .map(k => k + "=" + encodeURIComponent(findArgs[k])); if (queryString.length) { - return path + "?" + queryString.join('&'); + return `${path}${extension ? extension : ''}?${queryString.join('&')}`; } } else { // It's serializable as a string if not an object - return path + "/" + findArgs; + return `${path}/${findArgs}${extension ? extension : ''}`; } } return path; diff --git a/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 b/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 index 260d13ce288..986ac0ff6cb 100644 --- a/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 +++ b/app/assets/javascripts/discourse/components/basic-topic-list.js.es6 @@ -61,5 +61,11 @@ export default Ember.Component.extend({ } return false; } - } + }, + + actions: { + showInserted() { + this.sendAction('showInserted'); + }, + }, }); diff --git a/app/assets/javascripts/discourse/components/categories-and-top-topics.js.es6 b/app/assets/javascripts/discourse/components/categories-and-top-topics.js.es6 new file mode 100644 index 00000000000..4f1c8886929 --- /dev/null +++ b/app/assets/javascripts/discourse/components/categories-and-top-topics.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + classNames: ["categories-and-top"] +}); diff --git a/app/assets/javascripts/discourse/components/categories-boxes.js.es6 b/app/assets/javascripts/discourse/components/categories-boxes.js.es6 index fc63667da2c..7f364b9d9e2 100644 --- a/app/assets/javascripts/discourse/components/categories-boxes.js.es6 +++ b/app/assets/javascripts/discourse/components/categories-boxes.js.es6 @@ -1,4 +1,5 @@ import computed from 'ember-addons/ember-computed-decorators'; +import DiscourseURL from 'discourse/lib/url'; export default Ember.Component.extend({ tagName: "section", @@ -8,5 +9,14 @@ export default Ember.Component.extend({ anyLogos() { return this.get("categories").any((c) => { return !Ember.isEmpty(c.get('uploaded_logo.url')); }); return this.get("categories").any(c => !Ember.isEmpty(c.get('uploaded_logo.url'))); + }, + + click(e) { + if (!$(e.target).is('a')) { + const url = $(e.target).closest('.category-box').data("url"); + if (url) { + DiscourseURL.routeTo(url); + } + } } }); diff --git a/app/assets/javascripts/discourse/components/categories-topic-list.js.es6 b/app/assets/javascripts/discourse/components/categories-topic-list.js.es6 new file mode 100644 index 00000000000..9b102dc46c7 --- /dev/null +++ b/app/assets/javascripts/discourse/components/categories-topic-list.js.es6 @@ -0,0 +1,2 @@ +// Exists so plugins can use it +export default Ember.Component.extend(); diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6 deleted file mode 100644 index 379e326ac84..00000000000 --- a/app/assets/javascripts/discourse/components/category-drop.js.es6 +++ /dev/null @@ -1,152 +0,0 @@ -import { setting } from 'discourse/lib/computed'; -import computed from 'ember-addons/ember-computed-decorators'; - -var get = Ember.get; - -export default Ember.Component.extend({ - classNameBindings: ['category::no-category', 'categories:has-drop', 'categoryStyle'], - categoryStyle: setting('category_style'), - expanded: false, - - tagName: 'li', - - @computed('expanded') - expandIcon(expanded) { - return expanded ? 'd-drop-expanded' : 'd-drop-collapsed'; - }, - - allCategoriesUrl: function() { - if (this.get('subCategory')) { - return this.get('parentCategory.url') || "/"; - } else { - return "/"; - } - }.property('parentCategory.url', 'subCategory'), - - noCategoriesUrl: function() { - return this.get('parentCategory.url') + "/none"; - }.property('parentCategory.url'), - - allCategoriesLabel: function() { - if (this.get('subCategory')) { - return I18n.t('categories.all_subcategories', {categoryName: this.get('parentCategory.name')}); - } - return I18n.t('categories.all'); - }.property('category'), - - dropdownButtonClass: function() { - let result = 'dropdown-header category-dropdown-button'; - if (Em.isNone(this.get('category'))) { - result += ' home'; - } - return result; - }.property('category'), - - categoryColor: function() { - var category = this.get('category'); - - if (category) { - var color = get(category, 'color'); - - if (color) { - var style = ""; - if (color) { style += "background-color: #" + color + ";"; } - return style.htmlSafe(); - } - } - - return "background-color: #eee;".htmlSafe(); - }.property('category'), - - badgeStyle: function() { - let category = this.get('category'); - - const categoryStyle = this.siteSettings.category_style; - if (categoryStyle === 'bullet') { - return; - } - - if (category) { - let color = get(category, 'color'); - let textColor = get(category, 'text_color'); - - if (color || textColor) { - let style = ""; - if (color) { - if (categoryStyle === "bar") { - style += `border-color: #${color};`; - } else if (categoryStyle === "box") { - style += `background-color: #${color};`; - if (textColor) { style += "color: #" + textColor + "; "; } - } - } - - return style.htmlSafe(); - } - } - - if (categoryStyle === 'box') { - return "background-color: #eee; color: #333".htmlSafe(); - } - }.property('category'), - - clickEventName: function() { - return "click.category-drop-" + (this.get('category.id') || "all"); - }.property('category.id'), - - actions: { - expand: function() { - var self = this; - - if(!this.get('renderCategories')){ - this.set('renderCategories',true); - Em.run.next(function(){ - self.send('expand'); - }); - return; - } - - if (this.get('expanded')) { - this.close(); - return; - } - - if (this.get('categories')) { - this.set('expanded', true); - } - var $dropdown = this.$()[0]; - - this.$('a[data-drop-close]').on('click.category-drop', function() { - self.close(); - }); - - Em.run.next(function(){ - self.$('.cat a').add('html').on(self.get('clickEventName'), function(e) { - var $target = $(e.target), - closest = $target.closest($dropdown); - - if ($(e.currentTarget).hasClass('badge-wrapper')){ - self.close(); - } - - return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close(); - }); - }); - } - }, - - removeEvents: function(){ - $('html').off(this.get('clickEventName')); - this.$('a[data-drop-close]').off('click.category-drop'); - }, - - close: function() { - this.removeEvents(); - this.set('expanded', false); - }, - - willDestroyElement: function() { - this.removeEvents(); - } - -}); diff --git a/app/assets/javascripts/discourse/components/category-title-before.js.es6 b/app/assets/javascripts/discourse/components/category-title-before.js.es6 new file mode 100644 index 00000000000..9250c1ae73b --- /dev/null +++ b/app/assets/javascripts/discourse/components/category-title-before.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: '' +}); diff --git a/app/assets/javascripts/discourse/components/composer-action-title.js.es6 b/app/assets/javascripts/discourse/components/composer-action-title.js.es6 new file mode 100644 index 00000000000..3f9483595f2 --- /dev/null +++ b/app/assets/javascripts/discourse/components/composer-action-title.js.es6 @@ -0,0 +1,77 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { + PRIVATE_MESSAGE, + CREATE_TOPIC, + CREATE_SHARED_DRAFT, + REPLY, + EDIT, + EDIT_SHARED_DRAFT +} from "discourse/models/composer"; +import { iconHTML } from 'discourse-common/lib/icon-library'; + +const TITLES = { + [PRIVATE_MESSAGE]: 'topic.private_message', + [CREATE_TOPIC]: 'topic.create_long', + [CREATE_SHARED_DRAFT]: 'composer.create_shared_draft', + [EDIT_SHARED_DRAFT]: 'composer.edit_shared_draft' +}; + +export default Ember.Component.extend({ + classNames: ["composer-action-title"], + options: Ember.computed.alias("model.replyOptions"), + action: Ember.computed.alias("model.action"), + isEditing: Ember.computed.equal("action", EDIT), + + @computed("options", "action") + actionTitle(opts, action) { + if (TITLES[action]) { + return I18n.t(TITLES[action]); + } + + switch (action) { + case REPLY: + if (opts.userAvatar && opts.userLink) { + return this._formatReplyToUserPost(opts.userAvatar, opts.userLink); + } else if (opts.topicLink) { + return this._formatReplyToTopic(opts.topicLink); + } + case EDIT: + if (opts.userAvatar && opts.userLink && opts.postLink) { + return this._formatEditUserPost( + opts.userAvatar, + opts.userLink, + opts.postLink, + opts.originalUser + ); + } + }; + }, + + _formatEditUserPost(userAvatar, userLink, postLink, originalUser) { + let editTitle = ` + ${postLink.anchor} + ${userAvatar} + ${userLink.anchor} + `; + + if (originalUser) { + editTitle += ` + ${iconHTML("mail-forward", { class: "reply-to-glyph" })} + ${originalUser.avatar} + ${originalUser.username} + `; + } + + return editTitle.htmlSafe(); + }, + + _formatReplyToTopic(link) { + return `${link.anchor}`.htmlSafe(); + }, + + _formatReplyToUserPost(avatar, link) { + const htmlLink = `${link.anchor}`; + return `${avatar}${htmlLink}`.htmlSafe(); + }, + +}); diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6 index 8df136d3724..e868268c350 100644 --- a/app/assets/javascripts/discourse/components/composer-body.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-body.js.es6 @@ -15,6 +15,7 @@ export default Ember.Component.extend(KeyEnterEscape, { 'composer.createdPost:created-post', 'composer.creatingTopic:topic', 'composer.whisper:composing-whisper', + 'composer.sharedDraft:composing-shared-draft', 'showPreview:show-preview:hide-preview', 'currentUserPrimaryGroupClass'], diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 968e1ade9a3..86b4d39f87d 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -13,6 +13,7 @@ import { tinyAvatar, displayErrorForUpload, getUploadMarkdown, validateUploadedFiles, + authorizesOneOrMoreImageExtensions, formatUsername, clipboardData } from 'discourse/lib/utilities'; @@ -36,6 +37,21 @@ export default Ember.Component.extend({ return `[${I18n.t('uploading')}]() `; }, + @computed('composer.requiredCategoryMissing') + replyPlaceholder(requiredCategoryMissing) { + if (requiredCategoryMissing) { + return 'composer.reply_placeholder_choose_category'; + } else { + const key = authorizesOneOrMoreImageExtensions() ? "reply_placeholder" : "reply_placeholder_no_images"; + return `composer.${key}`; + } + }, + + @computed('composer.requiredCategoryMissing', 'composer.replyLength') + disableTextarea(requiredCategoryMissing, replyLength) { + return requiredCategoryMissing && replyLength === 0; + }, + @observes('composer.uploadCancelled') _cancelUpload() { if (!this.get('composer.uploadCancelled')) { return; } @@ -365,7 +381,13 @@ export default Ember.Component.extend({ post.set('refreshedPost', true); } - $oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id)); + $oneboxes.each((_, o) => load({ + elem: o, + refresh, + ajax, + categoryId: this.get('composer.category.id'), + topicId: this.get('composer.topic.id') + })); }, _warnMentionedGroups($preview) { @@ -403,8 +425,16 @@ export default Ember.Component.extend({ let name = $e.data('name'); if (found.indexOf(name) === -1) { - this.sendAction('cannotSeeMention', [{ name: name }]); - found.push(name); + + // add a delay to allow for typing, so you don't open the warning right away + // previously we would warn after @bob even if you were about to mention @bob2 + Em.run.later(this, () => { + if ($preview.find('.mention.cannot-see[data-name="' + name + '"]').length > 0) { + this.sendAction('cannotSeeMention', [{ name: name }]); + found.push(name); + } + }, 2000); + } }); @@ -662,13 +692,15 @@ export default Ember.Component.extend({ unshift: true }); - toolbar.addButton({ - id: 'upload', - group: 'insertions', - icon: 'upload', - title: 'upload', - sendAction: 'showUploadModal' - }); + if (this.get('allowUpload')) { + toolbar.addButton({ + id: 'upload', + group: 'insertions', + icon: 'upload', + title: 'upload', + sendAction: 'showUploadModal' + }); + } toolbar.addButton({ id: 'options', diff --git a/app/assets/javascripts/discourse/components/composer-messages.js.es6 b/app/assets/javascripts/discourse/components/composer-messages.js.es6 index 6f791315ab9..25cbad063b1 100644 --- a/app/assets/javascripts/discourse/components/composer-messages.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-messages.js.es6 @@ -1,5 +1,7 @@ import LinkLookup from 'discourse/lib/link-lookup'; +let _messagesCache = {}; + export default Ember.Component.extend({ classNameBindings: [':composer-popup-container', 'hidden'], checkedMessages: false, @@ -165,7 +167,9 @@ export default Ember.Component.extend({ if (topicId) { args.topic_id = topicId; } if (postId) { args.post_id = postId; } - composer.store.find('composer-message', args).then(messages => { + const cacheKey = `${args.composer_action}${args.topic_id}${args.post_id}`; + + const processMessages = messages => { if (this.isDestroying || this.isDestroyed) { return; } // Checking composer messages on replies can give us a list of links to check for @@ -177,6 +181,15 @@ export default Ember.Component.extend({ this.set('checkedMessages', true); const queuedForTyping = this.get('queuedForTyping'); messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg)); - }); + }; + + if (_messagesCache.cacheKey === cacheKey) { + processMessages(_messagesCache.messages); + } else { + composer.store.find('composer-message', args).then(messages => { + _messagesCache = {messages, cacheKey}; + processMessages(messages); + }); + } } }); diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index 2a828a9483a..9fc446930af 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -53,7 +53,11 @@ export default Ember.Component.extend({ if (this.get('autoPosted') || !this.get('watchForLink')) { return; } if (Ember.testing) { - this._checkForUrl(); + Em.run.next(() => + // not ideal but we don't want to run this in current + // runloop to avoid an error in console + this._checkForUrl() + ); } else { Ember.run.debounce(this, this._checkForUrl, 500); } @@ -80,7 +84,14 @@ export default Ember.Component.extend({ const link = document.createElement('a'); link.href = this.get('composer.title'); - let loadOnebox = load(link, false, ajax, this.currentUser.id, true); + const loadOnebox = load({ + elem: link, + refresh: false, + ajax, + synchronous: true, + categoryId: this.get('composer.category.id'), + topicId: this.get('composer.topic.id') + }); if (loadOnebox && loadOnebox.then) { loadOnebox.then( () => { diff --git a/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 new file mode 100644 index 00000000000..dd8f321635c --- /dev/null +++ b/app/assets/javascripts/discourse/components/conditional-loading-section.js.es6 @@ -0,0 +1,9 @@ +export default Ember.Component.extend({ + classNames: ["conditional-loading-section"], + + classNameBindings: ["isLoading"], + + isLoading: false, + + title: I18n.t("conditional_loading_section.loading") +}); diff --git a/app/assets/javascripts/discourse/components/create-topic-button.js.es6 b/app/assets/javascripts/discourse/components/create-topic-button.js.es6 index a792b83a291..c450bf72069 100644 --- a/app/assets/javascripts/discourse/components/create-topic-button.js.es6 +++ b/app/assets/javascripts/discourse/components/create-topic-button.js.es6 @@ -1 +1,4 @@ -export default Ember.Component.extend({ tagName: '' }); +export default Ember.Component.extend({ + tagName: '', + label: 'topic.create' +}); diff --git a/app/assets/javascripts/discourse/components/custom-html.js.es6 b/app/assets/javascripts/discourse/components/custom-html.js.es6 index c5df7fb51ed..f6114745f5d 100644 --- a/app/assets/javascripts/discourse/components/custom-html.js.es6 +++ b/app/assets/javascripts/discourse/components/custom-html.js.es6 @@ -2,6 +2,8 @@ import { getCustomHTML } from 'discourse/helpers/custom-html'; import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ + triggerAppEvent: null, + init() { this._super(); const name = this.get('name'); @@ -16,5 +18,19 @@ export default Ember.Component.extend({ this.set('layoutName', name); } } + }, + + didInsertElement() { + this._super(); + if (this.get('triggerAppEvent') === 'true') { + this.appEvents.trigger(`inserted-custom-html:${this.get('name')}`); + } + }, + + willDestroyElement() { + this._super(); + if (this.get('triggerAppEvent') === 'true') { + this.appEvents.trigger(`destroyed-custom-html:${this.get('name')}`); + } } }); diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index c56e1b6b34a..464a434bf11 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -6,7 +6,7 @@ export default Ember.Component.extend({ tagName: 'button', classNameBindings: [':btn', 'noText', 'btnType'], - attributeBindings: ['disabled', 'translatedTitle:title', 'tabindex'], + attributeBindings: ['disabled', 'translatedTitle:title', 'translatedTitle:aria-label', 'tabindex'], btnIcon: Ember.computed.notEmpty('icon'), @@ -23,7 +23,7 @@ export default Ember.Component.extend({ @computed("title") translatedTitle(title) { - return title ? I18n.t(title) : this.get('translatedLabel'); + if (title) return I18n.t(title); }, @computed("label") diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 3d357ec2255..d320767144c 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -8,9 +8,11 @@ import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji'; import { emojiUrlFor } from 'discourse/lib/text'; import { getRegister } from 'discourse-common/lib/get-owner'; import { findRawTemplate } from 'discourse/lib/raw-templates'; +import { siteDir } from 'discourse/lib/text-direction'; import { determinePostReplaceSelection, clipboardData } from 'discourse/lib/utilities'; import toMarkdown from 'discourse/lib/to-markdown'; import deprecated from 'discourse-common/lib/deprecated'; +import { wantsNewWindow } from 'discourse/lib/intercept-click'; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -44,7 +46,8 @@ const isInside = (text, regex) => { class Toolbar { - constructor(site) { + constructor(opts) { + const { site, siteSettings } = opts; this.shortcuts = {}; this.groups = [ @@ -73,7 +76,14 @@ class Toolbar { perform: e => e.applySurround('_', '_', 'italic_text') }); - this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'}); + if (opts.showLink) { + this.addButton({ + id: 'link', + group: 'insertions', + shortcut: 'K', + action: 'showLinkModal' + }); + } this.addButton({ id: 'quote', @@ -107,6 +117,17 @@ class Toolbar { perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item') }); + if (siteSettings.support_mixed_text_direction) { + this.addButton({ + id: 'toggle-direction', + group: 'extras', + icon: 'exchange', + shortcut: 'Shift+6', + title: 'composer.toggle_direction', + perform: e => e.toggleDirection(), + }); + } + if (site.mobileView) { this.groups.push({group: 'mobileExtras', buttons: []}); } @@ -188,6 +209,7 @@ export default Ember.Component.extend({ lastSel: null, _mouseTrap: null, emojiPickerIsActive: false, + showLink: true, @computed('placeholder') placeholderTranslated(placeholder) { @@ -237,7 +259,15 @@ export default Ember.Component.extend({ // disable clicking on links in the preview this.$('.d-editor-preview').on('click.preview', e => { - if ($(e.target).is("a")) { + if (wantsNewWindow(e)) { return; } + const $target = $(e.target); + if ($target.is("a.mention")) { + this.appEvents.trigger('click.discourse-preview-user-card-mention', $target); + } + if ($target.is("a.mention-group")) { + this.appEvents.trigger('click.discourse-preview-group-card-mention-group', $target); + } + if ($target.is("a")) { e.preventDefault(); return false; } @@ -254,6 +284,7 @@ export default Ember.Component.extend({ @on('willDestroyElement') _shutDown() { if (this.get('composerEvents')) { + this.appEvents.off('composer:insert-block'); this.appEvents.off('composer:insert-text'); this.appEvents.off('composer:replace-text'); } @@ -266,7 +297,9 @@ export default Ember.Component.extend({ @computed toolbar() { - const toolbar = new Toolbar(this.site); + const toolbar = new Toolbar( + this.getProperties('site', 'siteSettings', 'showLink') + ); _createCallbacks.forEach(cb => cb(toolbar)); this.sendAction('extraButtons', toolbar); return toolbar; @@ -312,6 +345,9 @@ export default Ember.Component.extend({ return obj.text; }, dataSource(term) { + if (term.match(/\s/)) { + return null; + } return searchCategoryTag(term, siteSettings); }, triggerRule(textarea, opts) { @@ -333,7 +369,11 @@ export default Ember.Component.extend({ }, onKeyUp(text, cp) { - return text.substring(0, cp).match(/(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/g); + const matches = /(?:^|[^a-z])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(text.substring(0, cp)); + + if (matches && matches[1]) { + return [ matches[1] ]; + } }, transformComplete(v) { @@ -646,6 +686,14 @@ export default Ember.Component.extend({ return null; }, + _toggleDirection() { + const $textArea = $(".d-editor-input"); + let currentDir = $textArea.attr('dir') ? $textArea.attr('dir') : siteDir(), + newDir = currentDir === 'ltr' ? 'rtl' : 'ltr'; + + $textArea.attr('dir', newDir).focus(); + }, + paste(e) { if (!$(".d-editor-input").is(":focus")) { return; @@ -714,6 +762,8 @@ export default Ember.Component.extend({ }, toolbarButton(button) { + if (this.get('disabled')) { return; } + const selected = this._getSelected(button.trimLeading); const toolbarEvent = { selected, @@ -723,6 +773,7 @@ export default Ember.Component.extend({ addText: text => this._addText(selected, text), replaceText: text => this._addText({pre: '', post: ''}, text), getText: () => this.get('value'), + toggleDirection: () => this._toggleDirection(), }; if (button.sendAction) { @@ -733,11 +784,20 @@ export default Ember.Component.extend({ }, showLinkModal() { + if (this.get('disabled')) { return; } + this._lastSel = this._getSelected(); + + if (this._lastSel) { + this.set("linkText", this._lastSel.value.trim()); + } + this.set('insertLinkHidden', false); }, formatCode() { + if (this.get('disabled')) { return; } + const sel = this._getSelected('', { lineVal: true }); const selValue = sel.value; const hasNewLine = selValue.indexOf("\n") !== -1; @@ -791,6 +851,7 @@ export default Ember.Component.extend({ }, emoji() { + if (this.get('disabled')) { return; } this.set('emojiPickerIsActive', !this.get('emojiPickerIsActive')); } } diff --git a/app/assets/javascripts/discourse/components/d-modal-body.js.es6 b/app/assets/javascripts/discourse/components/d-modal-body.js.es6 index 660456db7f5..04c9754dd88 100644 --- a/app/assets/javascripts/discourse/components/d-modal-body.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal-body.js.es6 @@ -14,11 +14,13 @@ export default Ember.Component.extend({ Ember.run.scheduleOnce('afterRender', this, this._afterFirstRender); this.appEvents.on('modal-body:flash', msg => this._flash(msg)); + this.appEvents.on('modal-body:clearFlash', () => this._clearFlash()); }, willDestroyElement() { this._super(); this.appEvents.off('modal-body:flash'); + this.appEvents.off('modal-body:clearFlash'); }, _afterFirstRender() { @@ -40,15 +42,23 @@ export default Ember.Component.extend({ this.getProperties( 'title', 'rawTitle', - 'fixed' + 'fixed', + 'subtitle', + 'rawSubtitle' ) ); }, + _clearFlash() { + $('#modal-alert').hide().removeClass('alert-error', 'alert-success'); + }, + _flash(msg) { - $('#modal-alert').hide() - .removeClass('alert-error', 'alert-success') - .addClass(`alert alert-${msg.messageClass || 'success'}`).html(msg.text || '') - .fadeIn(); + this._clearFlash(); + + $('#modal-alert') + .addClass(`alert alert-${msg.messageClass || 'success'}`) + .html(msg.text || '') + .fadeIn(); }, }); diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index c015ca6ba52..a57f4daea09 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -28,6 +28,7 @@ export default Ember.Component.extend({ this.appEvents.on('modal:body-shown', data => { if (this.isDestroying || this.isDestroyed) { return; } + if (data.fixed) { this.$().removeClass('hidden'); } @@ -37,6 +38,16 @@ export default Ember.Component.extend({ } else if (data.rawTitle) { this.set('title', data.rawTitle); } + + if (data.subtitle) { + this.set('subtitle', I18n.t(data.subtitle)); + } else if (data.rawSubtitle) { + this.set('subtitle', data.rawSubtitle); + } else { + // if no subtitle provided, makes sure the previous subtitle + // of another modal is not used + this.set('subtitle', null); + } }); }, diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index ff1e6ef40aa..313386c6219 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -13,6 +13,12 @@ export default Ember.Component.extend({ return this.site.get('categoriesList'); }, + @computed('hasDraft') + createTopicLabel(hasDraft) + { + return hasDraft ? 'topic.open_draft': 'topic.create'; + }, + @computed('category.can_edit') showCategoryEdit: canEdit => canEdit, diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6 index 709e1e51497..180c46c3623 100644 --- a/app/assets/javascripts/discourse/components/date-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/date-picker.js.es6 @@ -36,6 +36,9 @@ export default Ember.Component.extend({ @on("willDestroyElement") _destroy() { + if (this._picker) { + this._picker.destroy(); + } this._picker = null; }, diff --git a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 index d1ad4f28abb..9b981b6b8f3 100644 --- a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 +++ b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 @@ -1,6 +1,13 @@ import computed from 'ember-addons/ember-computed-decorators'; import KeyValueStore from 'discourse/lib/key-value-store'; -import { context } from 'discourse/lib/desktop-notifications'; +import { context, confirmNotification } from 'discourse/lib/desktop-notifications'; +import { + subscribe as subscribePushNotification, + unsubscribe as unsubscribePushNotification, + isPushNotificationsSupported, + keyValueStore as pushNotificationKeyValueStore, + userSubscriptionKey as pushNotificationUserSubscriptionKey +} from 'discourse/lib/push-notifications'; const keyValueStore = new KeyValueStore(context); @@ -28,11 +35,6 @@ export default Ember.Component.extend({ return typeof window.Notification === "undefined"; }, - @computed("isNotSupported", "notificationsPermission") - isDefaultPermission(isNotSupported, notificationsPermission) { - return isNotSupported ? false : notificationsPermission === "default"; - }, - @computed("isNotSupported", "notificationsPermission") isDeniedPermission(isNotSupported, notificationsPermission) { return isNotSupported ? false : notificationsPermission === "denied"; @@ -44,27 +46,65 @@ export default Ember.Component.extend({ }, @computed("isGrantedPermission", "notificationsDisabled") - isEnabled(isGrantedPermission, notificationsDisabled) { + isEnabledDesktop(isGrantedPermission, notificationsDisabled) { return isGrantedPermission ? !notificationsDisabled : false; }, - actions: { - requestPermission() { - Notification.requestPermission(() => this.propertyDidChange('notificationsPermission')); + @computed + isEnabledPush: { + set(value) { + const user = this.currentUser; + if(!user) { + return false; + } + pushNotificationKeyValueStore.setItem(pushNotificationUserSubscriptionKey(user), value); + return pushNotificationKeyValueStore.getItem(pushNotificationUserSubscriptionKey(user)); }, + get() { + const user = this.currentUser; + return user ? pushNotificationKeyValueStore.getItem(pushNotificationUserSubscriptionKey(user)) : false; + } + }, + isEnabled: Ember.computed.or("isEnabledDesktop", "isEnabledPush"), + + isPushNotificationsPreferred() { + if(!this.site.mobileView) { + return false; + } + return isPushNotificationsSupported(this.site.mobileView); + }, + + actions: { recheckPermission() { this.propertyDidChange('notificationsPermission'); }, turnoff() { - this.set('notificationsDisabled', 'disabled'); - this.propertyDidChange('notificationsPermission'); + if(this.get('isEnabledDesktop')) { + this.set('notificationsDisabled', 'disabled'); + this.propertyDidChange('notificationsPermission'); + } + if(this.get('isEnabledPush')) { + unsubscribePushNotification(this.currentUser, () => { + this.set("isEnabledPush", ''); + }); + } }, turnon() { - this.set('notificationsDisabled', ''); - this.propertyDidChange('notificationsPermission'); + if(this.isPushNotificationsPreferred()) { + subscribePushNotification(() => { + this.set("isEnabledPush", 'subscribed'); + }, this.siteSettings.vapid_public_key_bytes); + } + else { + this.set('notificationsDisabled', ''); + Notification.requestPermission(() => { + confirmNotification(); + this.propertyDidChange('notificationsPermission'); + }); + } } } }); diff --git a/app/assets/javascripts/discourse/components/directory-toggle.js.es6 b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 index 8da529957a4..f6a9de9b800 100644 --- a/app/assets/javascripts/discourse/components/directory-toggle.js.es6 +++ b/app/assets/javascripts/discourse/components/directory-toggle.js.es6 @@ -1,16 +1,22 @@ import { iconHTML } from 'discourse-common/lib/icon-library'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend(bufferedRender({ tagName: 'th', classNames: ['sortable'], attributeBindings: ['title'], rerenderTriggers: ['order', 'asc'], + labelKey: null, + + @computed("field", "labelKey") + title(field, labelKey) { + if (!labelKey) { + labelKey = `directory.${this.get('field')}`; + } - title: function() { - const labelKey = 'directory.' + this.get('field'); return I18n.t(labelKey + '_long', { defaultValue: I18n.t(labelKey) }); - }.property('field'), + }, buildBuffer(buffer) { const icon = this.get('icon'); @@ -19,7 +25,7 @@ export default Ember.Component.extend(bufferedRender({ } const field = this.get('field'); - buffer.push(I18n.t('directory.' + field)); + buffer.push(I18n.t(this.get('labelKey') || `directory.${field}`)); if (field === this.get('order')) { buffer.push(iconHTML(this.get('asc') ? 'chevron-up' : 'chevron-down')); diff --git a/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6 b/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6 new file mode 100644 index 00000000000..44e8bf9e596 --- /dev/null +++ b/app/assets/javascripts/discourse/components/discourse-linked-text.js.es6 @@ -0,0 +1,18 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + tagName: 'span', + + @computed("text") + translatedText(text) { + if (text) return I18n.t(text); + }, + + click(event) { + if (event.target.tagName.toUpperCase() === 'A') { + this.sendAction("action", this.get("actionParam")); + } + + return false; + } +}); diff --git a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 index 421972b834c..4b853e541da 100644 --- a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 @@ -46,5 +46,12 @@ export default Ember.Component.extend({ } this.set("topicTimer.updateTime", time); - } + }, + + @observes("selection") + _updateBasedOnLastPost() { + if (!this.get('autoClose')) { + this.set('topicTimer.based_on_last_post', false); + } + }, }); diff --git a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 b/app/assets/javascripts/discourse/components/emoji-picker.js.es6 index 03efe27a53c..0bf7f9f7bf3 100644 --- a/app/assets/javascripts/discourse/components/emoji-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/emoji-picker.js.es6 @@ -2,8 +2,8 @@ import { on, observes } from "ember-addons/ember-computed-decorators"; import { findRawTemplate } from "discourse/lib/raw-templates"; import { emojiUrlFor } from "discourse/lib/text"; import KeyValueStore from "discourse/lib/key-value-store"; -import { emojis } from "pretty-text/emoji/data"; -import { extendedEmojiList, isSkinTonableEmoji } from "pretty-text/emoji"; +import { extendedEmojiList, isSkinTonableEmoji, emojiSearch } from "pretty-text/emoji"; +const { run } = Ember; const keyValueStore = new KeyValueStore("discourse_emojis_"); const EMOJI_USAGE = "emojiUsage"; @@ -44,13 +44,13 @@ export default Ember.Component.extend({ this.set("selectedDiversity", keyValueStore.getObject(EMOJI_SELECTED_DIVERSITY) || 1); this.set("recentEmojis", keyValueStore.getObject(EMOJI_USAGE) || []); - Ember.run.scheduleOnce("afterRender", this, function() { + run.scheduleOnce("afterRender", this, function() { this._bindEvents(); - this._sectionLoadingCheck(); this._loadCategoriesEmojis(); this._positionPicker(); this._scrollTo(); this._updateSelectedDiversity(); + this._checkVisibleSection(true); }); }, @@ -83,13 +83,14 @@ export default Ember.Component.extend({ @on("didUpdateAttrs") _setState() { - this.get("active") === true ? this.show() : this.close(); + this.get("active") ? this.show() : this.close(); }, @observes("filter") filterChanged() { this.$filter.find(".clear-filter").toggle(!_.isEmpty(this.get("filter"))); - Ember.run.debounce(this, this._filterEmojisList, 250); + const filterDelay = this.site.isMobileDevice ? 400 : 250; + run.debounce(this, this._filterEmojisList, filterDelay); }, @observes("selectedDiversity") @@ -105,6 +106,7 @@ export default Ember.Component.extend({ } this._updateSelectedDiversity(); + this._checkVisibleSection(true); }, @observes("recentEmojis") @@ -146,11 +148,6 @@ export default Ember.Component.extend({ .addClass("selected"); }, - _sectionLoadingCheck() { - this._checkTimeout = setTimeout(() => { this._sectionLoadingCheck(); }, 500); - Ember.run.throttle(this, this._checkVisibleSection, 100); - }, - _loadCategoriesEmojis() { $.each(this.$picker.find(".categories-column button.emoji"), (_, button) => { const $button = $(button); @@ -177,13 +174,17 @@ export default Ember.Component.extend({ _bindModalClick() { this.$modal.on("click", () => this.set("active", false)); - this.$(document).on("click.emoji-picker", (event) => { - const onPicker = $(event.target).parents(".emoji-picker").length === 1; - const onGrippie = event.target.className.indexOf("grippie") > -1; - if(!onPicker && !onGrippie) { - this.set("active", false); - return false; + $('html').on("mouseup.emoji-picker", event => { + let $target = $(event.target); + if ($target.closest(".emoji-picker").length || + $target.closest('.emoji.btn').length || + $target.hasClass('grippie')) { + return; } + + // Close the popup if clicked outside + this.set("active", false); + return false; }); }, @@ -191,22 +192,19 @@ export default Ember.Component.extend({ _unbindEvents() { this.$().off(); this.$(window).off("resize"); - this.$modal.off("click"); + clearInterval(this._refreshInterval); $("#reply-control").off("div-resizing"); - this.$(document).off("click.emoji-picker"); + $('html').off("mouseup.emoji-picker"); }, _filterEmojisList() { if (this.get("filter") === "") { this.$filter.find("input[name='filter']").val(""); this.$results.empty().hide(); - this.$list.show(); + this.$list.css("visibility", "visible"); } else { const lowerCaseFilter = this.get("filter").toLowerCase(); - const filterableEmojis = emojis.concat(_.keys(extendedEmojiList())); - const filteredCodes = _.filter(filterableEmojis, code => { - return code.indexOf(lowerCaseFilter) > -1; - }).slice(0, 30); + const filteredCodes = emojiSearch(lowerCaseFilter, { maxResults: 30}); this.$results.empty().html( _.map(filteredCodes, (code) => { const hasDiversity = isSkinTonableEmoji(code); @@ -217,7 +215,7 @@ export default Ember.Component.extend({ ).show(); this._bindHover(this.$results); this._bindEmojiClick(this.$results); - this.$list.hide(); + this.$list.css("visibility", "hidden"); } }, @@ -239,7 +237,7 @@ export default Ember.Component.extend({ this.$picker.find(".category-icon").on("click", "button.emoji", (event) => { this.set("filter", ""); this.$results.empty(); - this.$list.show(); + this.$list.css("visibility", "visible"); const section = $(event.currentTarget).data("section"); const $section = this.$list.find(`.section[data-section="${section}"]`); @@ -264,11 +262,11 @@ export default Ember.Component.extend({ _bindResizing() { this.$(window).on("resize", () => { - Ember.run.throttle(this, this._positionPicker, 16); + run.throttle(this, this._positionPicker, 16); }); $("#reply-control").on("div-resizing", () => { - Ember.run.throttle(this, this._positionPicker, 16); + run.throttle(this, this._positionPicker, 16); }); }, @@ -318,18 +316,27 @@ export default Ember.Component.extend({ }, _bindSectionsScroll() { - this.$list.on("scroll", () => { - this.scrollPosition = this.$list.scrollTop(); - Ember.run.throttle(this, this._checkVisibleSection, 150); - }); + let onScroll = () => { + run.debounce(this, this._checkVisibleSection, 50); + }; + + this.$list.on("scroll", onScroll); + this._refreshInterval = setInterval(onScroll, 100); }, - _checkVisibleSection() { + _checkVisibleSection(force) { // make sure we stop loading if picker has been removed if(!this.$picker) { return; } + const newPosition = this.$list.scrollTop(); + if (newPosition === this.scrollPosition && !force) { + return; + } + + this.scrollPosition = newPosition; + const $sections = this.$list.find(".section"); const listHeight = this.$list.innerHeight(); let $selectedSection; @@ -362,15 +369,19 @@ export default Ember.Component.extend({ } const listHeight = this.$list.innerHeight(); + + this.$visibleSections.forEach(visibleSection => { const $unloadedEmojis = $(visibleSection).find("button.emoji[data-loaded!='1']"); $.each($unloadedEmojis, (_, button) => { - const $button = $(button); - const buttonTop = $button.position().top; - const buttonHeight = $button.height(); - if(buttonTop + buttonHeight > 0 && buttonTop - buttonHeight < listHeight) { - this._setButtonBackground($button); + let offsetTop = button.offsetTop; + + if (offsetTop < this.scrollPosition + listHeight + 200) { + if (offsetTop + 200 > this.scrollPosition) { + const $button = $(button); + this._setButtonBackground($button); + } } }); }); @@ -415,12 +426,12 @@ export default Ember.Component.extend({ const mobilePositioning = options => { let attributes = { - width: windowWidth - 12, - marginLeft: 5, - marginTop: -130, + width: windowWidth, + marginLeft: 0, + marginTop: "auto", left: 0, bottom: "", - top: "50%", + top: 0, display: "flex" }; @@ -445,7 +456,7 @@ export default Ember.Component.extend({ this.$picker.css(_.merge(attributes, options)); }; - if(Ember.testing || this.get("automaticPositioning") === false) { + if(Ember.testing || !this.get("automaticPositioning")) { desktopPositioning(); return; } @@ -510,7 +521,7 @@ export default Ember.Component.extend({ this.$list.scrollTop(yPosition); // if we don’t actually scroll we need to force it - if(yPosition === 0) { + if (yPosition === 0) { this.$list.scroll(); } }, @@ -521,19 +532,31 @@ export default Ember.Component.extend({ }, _setButtonBackground(button, diversity) { - const $button = $(button); - const code = this._codeWithDiversity( - $button.attr("title"), - diversity || $button.hasClass("diversity") - ); - // force visual reloading if needed - if($button.css("background-image") !== "none") { - $button.css("background-image", ""); + if (!button) { + return; } - $button - .attr("data-loaded", 1) - .css("background-image", `url("${emojiUrlFor(code)}")`); + const $button = $(button); + button = $button[0]; + + // changing style can force layout events + // this could slow down timers and lead to + // chrome delaying the request + window.requestAnimationFrame(() =>{ + const code = this._codeWithDiversity( + $button.attr("title"), + diversity || $button.hasClass("diversity") + ); + + // // force visual reloading if needed + if(button.style.backgroundImage !== "none") { + button.style.backgroundImage = ""; + } + + button.style.backgroundImage = `url("${emojiUrlFor(code)}")`; + $button.attr("data-loaded", 1); + }); + }, }); diff --git a/app/assets/javascripts/discourse/components/flag-action-type.js.es6 b/app/assets/javascripts/discourse/components/flag-action-type.js.es6 index 7f0ffb70edc..fe2b5c601a0 100644 --- a/app/assets/javascripts/discourse/components/flag-action-type.js.es6 +++ b/app/assets/javascripts/discourse/components/flag-action-type.js.es6 @@ -34,13 +34,13 @@ export default Ember.Component.extend({ @computed('message.length') customMessageLengthClasses(messageLength) { - return (messageLength < Discourse.SiteSettings.min_private_message_post_length) ? "too-short" : "ok"; + return (messageLength < Discourse.SiteSettings.min_personal_message_post_length) ? "too-short" : "ok"; }, @computed('message.length') customMessageLength(messageLength) { const len = messageLength || 0; - const minLen = Discourse.SiteSettings.min_private_message_post_length; + const minLen = Discourse.SiteSettings.min_personal_message_post_length; if (len === 0) { return I18n.t("flagging.custom_message.at_least", { count: minLen }); } else if (len < minLen) { diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6 index 06f7e68a166..9ea7f4104bd 100644 --- a/app/assets/javascripts/discourse/components/global-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/global-notice.js.es6 @@ -9,6 +9,11 @@ export default Ember.Component.extend(bufferedRender({ buildBuffer(buffer) { let notices = []; + if ($.cookie("dosp") === "1") { + $.cookie("dosp", null, { path: '/' }); + notices.push([I18n.t("forced_anonymous"), 'forced-anonymous']); + } + if (this.session.get('safe_mode')) { notices.push([I18n.t("safe_mode.enabled"), 'safe-mode']); } diff --git a/app/assets/javascripts/discourse/components/group-card-contents.js.es6 b/app/assets/javascripts/discourse/components/group-card-contents.js.es6 new file mode 100644 index 00000000000..e21410fa511 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-card-contents.js.es6 @@ -0,0 +1,83 @@ +import { setting } from 'discourse/lib/computed'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; +import CardContentsBase from 'discourse/mixins/card-contents-base'; +import CleansUp from 'discourse/mixins/cleans-up'; + +const maxMembersToDisplay = 10; + +export default Ember.Component.extend(CardContentsBase, CleansUp, { + elementId: 'group-card', + triggeringLinkClass: 'mention-group', + classNames: ['no-bg'], + classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'isFixed:fixed'], + allowBackgrounds: setting('allow_profile_backgrounds'), + showBadges: setting('enable_badges'), + + postStream: Ember.computed.alias('topic.postStream'), + viewingTopic: Ember.computed.match('currentPath', /^topic\./), + + showMoreMembers: Ember.computed.gt('moreMembersCount', 0), + + group: null, + + @computed('group.user_count', 'group.members.length') + moreMembersCount: (memberCount, maxMemberDisplay) => memberCount - maxMemberDisplay, + + @computed('group') + groupPath(group) { + return `${Discourse.BaseUri}/groups/${group.name}`; + }, + + _showCallback(username, $target) { + this.store.find("group", username).then(group => { + this.setProperties({ group, visible: true }); + this._positionCard($target); + if(!group.flair_url && !group.flair_bg_color) { + group.set('flair_url', 'fa-users'); + } + group.set('limit', maxMembersToDisplay); + return group.findMembers(); + }).catch(() => this._close()).finally(() => this.set('loading', null)); + }, + + didInsertElement() { + this._super(); + }, + + _close() { + this._super(); + this.setProperties({ + group: null, + }); + }, + + cleanUp() { + this._close(); + }, + + actions: { + close() { + this._close(); + }, + + cancelFilter() { + const postStream = this.get('postStream'); + postStream.cancelFilter(); + postStream.refresh(); + this._close(); + }, + + composePrivateMessage(...args) { + this.sendAction('composePrivateMessage', ...args); + }, + + messageGroup() { + this.sendAction('createNewMessageViaParams', this.get('group.name')); + }, + + showGroup() { + this.sendAction('showGroup', this.get('group')); + this._close(); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6 b/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6 index ce4113450b3..bb6cb51fe8a 100644 --- a/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6 +++ b/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6 @@ -34,7 +34,7 @@ export default Ember.Component.extend({ if (flairHexColor) style += `color: #${flairHexColor};`; - return style; + return Ember.String.htmlSafe(style); }, @computed('model.flairBackgroundHexColor') diff --git a/app/assets/javascripts/discourse/components/group-index-toggle.js.es6 b/app/assets/javascripts/discourse/components/group-index-toggle.js.es6 index f5b24684087..6967a502a81 100644 --- a/app/assets/javascripts/discourse/components/group-index-toggle.js.es6 +++ b/app/assets/javascripts/discourse/components/group-index-toggle.js.es6 @@ -7,11 +7,13 @@ export default Ember.Component.extend(bufferedRender({ rerenderTriggers: ['order', 'desc'], buildBuffer(buffer) { + buffer.push(""); buffer.push(I18n.t(this.get('i18nKey'))); if (this.get('field') === this.get('order')) { buffer.push(iconHTML(this.get('desc') ? 'chevron-down' : 'chevron-up')); } + buffer.push(""); }, click() { diff --git a/app/assets/javascripts/discourse/components/group-logs-filter.js.es6 b/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 similarity index 89% rename from app/assets/javascripts/discourse/components/group-logs-filter.js.es6 rename to app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 index ba4ccc3f742..d598bc568fe 100644 --- a/app/assets/javascripts/discourse/components/group-logs-filter.js.es6 +++ b/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 @@ -5,7 +5,7 @@ export default Ember.Component.extend({ @computed('type') label(type) { - return I18n.t(`groups.logs.${type}`); + return I18n.t(`groups.manage.logs.${type}`); }, @computed('value', 'type') diff --git a/app/assets/javascripts/discourse/components/group-logs-row.js.es6 b/app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/components/group-logs-row.js.es6 rename to app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6 diff --git a/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6 b/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6 new file mode 100644 index 00000000000..1055dc8913b --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-manage-save-button.js.es6 @@ -0,0 +1,25 @@ +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + saving: null, + + @computed('saving') + savingText(saving) { + if (saving) return I18n.t("saving"); + return saving ? I18n.t("saving") : I18n.t("save"); + }, + + actions: { + save() { + this.set('saving', true); + + return this.get('model').save() + .then(() => { + this.set("saved", true); + }) + .catch(popupAjaxError) + .finally(() => this.set('saving', false)); + } + }, +}); diff --git a/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6 b/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6 new file mode 100644 index 00000000000..0ba9bb5d1ce --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-member-dropdown.js.es6 @@ -0,0 +1,65 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; +import DropdownButton from 'discourse/components/dropdown-button'; +import computed from "ember-addons/ember-computed-decorators"; + +export default DropdownButton.extend({ + buttonExtraClasses: 'no-text', + title: '', + text: iconHTML('wrench'), + classNames: ['group-member-dropdown'], + + @computed("member.owner") + dropDownContent(isOwner) { + const items = [ + { + id: 'removeMember', + title: I18n.t('groups.members.remove_member'), + description: I18n.t( + 'groups.members.remove_member_description', + { username: this.get('member.username') } + ), + icon: 'user-times' + } + ]; + + if (this.currentUser && this.currentUser.admin) { + if (isOwner) { + items.push({ + id: 'removeOwner', + title: I18n.t('groups.members.remove_owner'), + description: I18n.t( + 'groups.members.remove_owner_description', + { username: this.get('member.username') } + ), + icon: 'shield' + }); + } else { + items.push({ + id: 'makeOwner', + title: I18n.t('groups.members.make_owner'), + description: I18n.t( + 'groups.members.make_owner_description', + { username: this.get('member.username') } + ), + icon: 'shield' + }); + } + } + + return items; + }, + + clicked(id) { + switch (id) { + case 'removeMember': + this.sendAction('removeMember', this.get('member')); + break; + case 'makeOwner': + this.sendAction('makeOwner', this.get('member.username')); + break; + case 'removeOwner': + this.sendAction('removeOwner', this.get('member')); + break; + } + } +}); diff --git a/app/assets/javascripts/discourse/components/group-members-input.js.es6 b/app/assets/javascripts/discourse/components/group-members-input.js.es6 index 691eb051bbc..1bb3ca048d7 100644 --- a/app/assets/javascripts/discourse/components/group-members-input.js.es6 +++ b/app/assets/javascripts/discourse/components/group-members-input.js.es6 @@ -55,7 +55,7 @@ export default Ember.Component.extend({ }, removeMember(member) { - const message = I18n.t("groups.edit.delete_member_confirm",{ + const message = I18n.t("groups.manage.delete_member_confirm",{ username: member.get("username"), group: this.get("model.name") }); diff --git a/app/assets/javascripts/discourse/components/group-membership-button.js.es6 b/app/assets/javascripts/discourse/components/group-membership-button.js.es6 index 99c5273e09b..52478ada28d 100644 --- a/app/assets/javascripts/discourse/components/group-membership-button.js.es6 +++ b/app/assets/javascripts/discourse/components/group-membership-button.js.es6 @@ -3,6 +3,8 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import showModal from 'discourse/lib/show-modal'; export default Ember.Component.extend({ + classNames: ["group-membership-button"], + @computed("model.public_admission", "userIsGroupUser") canJoinGroup(publicAdmission, userIsGroupUser) { return publicAdmission && !userIsGroupUser; @@ -13,13 +15,9 @@ export default Ember.Component.extend({ return publicExit && userIsGroupUser; }, - @computed("model.is_group_user", "model.id", "groupUserIds") - userIsGroupUser(isGroupUser, groupId, groupUserIds) { - if (isGroupUser !== undefined) { - return isGroupUser; - } else { - return !!groupUserIds && groupUserIds.includes(groupId); - } + @computed("model.is_group_user") + userIsGroupUser(isGroupUser) { + return !!isGroupUser; }, _showLoginModal() { diff --git a/app/assets/javascripts/discourse/components/group-navigation.js.es6 b/app/assets/javascripts/discourse/components/group-navigation.js.es6 index 35f5f238e47..e164f8c987a 100644 --- a/app/assets/javascripts/discourse/components/group-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/group-navigation.js.es6 @@ -1,15 +1,3 @@ -import computed from 'ember-addons/ember-computed-decorators'; - export default Ember.Component.extend({ tagName: '', - - @computed('group') - availableTabs(group) { - return this.get('tabs').filter(t => { - if (t.admin) { - return this.currentUser ? this.currentUser.canManageGroup(group) : false; - } - return true; - }); - } }); diff --git a/app/assets/javascripts/discourse/components/group-post.js.es6 b/app/assets/javascripts/discourse/components/group-post.js.es6 new file mode 100644 index 00000000000..51e67b8490e --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-post.js.es6 @@ -0,0 +1,6 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + @computed('post.url') + postUrl: Discourse.getURL +}); diff --git a/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6 new file mode 100644 index 00000000000..8316c6069c9 --- /dev/null +++ b/app/assets/javascripts/discourse/components/groups-form-interaction-fields.js.es6 @@ -0,0 +1,23 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + visibilityLevelOptions: [ + { name: I18n.t("admin.groups.manage.interaction.visibility_levels.public"), value: 0 }, + { name: I18n.t("admin.groups.manage.interaction.visibility_levels.members"), value: 1 }, + { name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"), value: 2 }, + { name: I18n.t("admin.groups.manage.interaction.visibility_levels.owners"), value: 3 } + ], + + aliasLevelOptions: [ + { name: I18n.t("groups.alias_levels.nobody"), value: 0 }, + { name: I18n.t("groups.alias_levels.only_admins"), value: 1 }, + { name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 }, + { name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 }, + { name: I18n.t("groups.alias_levels.everyone"), value: 99 } + ], + + @computed('siteSettings.email_in', 'model.automatic', 'currentUser.admin') + showEmailSettings(emailIn, automatic, isAdmin) { + return emailIn && isAdmin && !automatic; + } +}); diff --git a/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6 new file mode 100644 index 00000000000..e9c7e8a37e3 --- /dev/null +++ b/app/assets/javascripts/discourse/components/groups-form-membership-fields.js.es6 @@ -0,0 +1,20 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + trustLevelOptions: [ + { name: I18n.t("admin.groups.manage.membership.trust_levels_none"), value: 0 }, + { name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 } + ], + + @computed('model.visibility_level', 'model.public_admission') + disableMembershipRequestSetting(visibility_level, publicAdmission) { + visibility_level = parseInt(visibility_level); + return (visibility_level !== 0) || publicAdmission; + }, + + @computed('model.visibility_level', 'model.allow_membership_requests') + disablePublicSetting(visibility_level, allowMembershipRequests) { + visibility_level = parseInt(visibility_level); + return (visibility_level !== 0) || allowMembershipRequests; + } +}); diff --git a/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 b/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 new file mode 100644 index 00000000000..c62f7710981 --- /dev/null +++ b/app/assets/javascripts/discourse/components/groups-form-profile-fields.js.es6 @@ -0,0 +1,89 @@ +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import User from "discourse/models/user"; +import InputValidation from 'discourse/models/input-validation'; +import debounce from 'discourse/lib/debounce'; + +export default Ember.Component.extend({ + disableSave: null, + nameInput: null, + + didInsertElement() { + this._super(); + const name = this.get('model.name'); + + if (name) { + this.set("nameInput", name); + } else { + this.set('disableSave', true); + } + }, + + @computed('basicNameValidation', 'uniqueNameValidation') + nameValidation(basicNameValidation, uniqueNameValidation) { + return uniqueNameValidation ? uniqueNameValidation : basicNameValidation; + }, + + @observes("nameInput") + _validateName() { + name = this.get('nameInput'); + if (name === this.get('model.name')) return; + + if (name === undefined) { + return this._failedInputValidation(); + }; + + if (name === "") { + this.set('uniqueNameValidation', null); + return this._failedInputValidation(I18n.t('admin.groups.new.name.blank')); + } + + if (name.length < this.siteSettings.min_username_length) { + return this._failedInputValidation(I18n.t('admin.groups.new.name.too_short')); + } + + if (name.length > this.siteSettings.max_username_length) { + return this._failedInputValidation(I18n.t('admin.groups.new.name.too_long')); + } + + this.checkGroupName(); + + return this._failedInputValidation(I18n.t('admin.groups.new.name.checking')); + }, + + checkGroupName: debounce(function() { + name = this.get('nameInput'); + if (Ember.isEmpty(name)) return; + + User.checkUsername(name).then(response => { + const validationName = 'uniqueNameValidation'; + + if (response.available) { + this.set(validationName, InputValidation.create({ + ok: true, + reason: I18n.t('admin.groups.new.name.available') + })); + + this.set('disableSave', false); + this.set('model.name', this.get('nameInput')); + } else { + let reason; + + if (response.errors) { + reason = response.errors.join(' '); + } else { + reason = I18n.t('admin.groups.new.name.not_available'); + } + + this.set(validationName, this._failedInputValidation(reason)); + } + }); + }, 500), + + _failedInputValidation(reason) { + this.set('disableSave', true); + + const options = { failed: true }; + if (reason) options.reason = reason; + this.set('basicNameValidation', InputValidation.create(options)); + }, +}); diff --git a/app/assets/javascripts/discourse/components/groups-info.js.es6 b/app/assets/javascripts/discourse/components/groups-info.js.es6 new file mode 100644 index 00000000000..db6f78d73df --- /dev/null +++ b/app/assets/javascripts/discourse/components/groups-info.js.es6 @@ -0,0 +1,10 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + tagName: 'span', + + @computed('group.full_name', 'group.title') + showFullName(fullName, title) { + return fullName && fullName.length && fullName !== title; + } +}); diff --git a/app/assets/javascripts/discourse/components/login-buttons.js.es6 b/app/assets/javascripts/discourse/components/login-buttons.js.es6 index 4c68d1740f2..aa90e565c73 100644 --- a/app/assets/javascripts/discourse/components/login-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/login-buttons.js.es6 @@ -13,8 +13,12 @@ export default Ember.Component.extend({ }, actions: { - externalLogin: function(provider) { - this.sendAction('action', provider); + emailLogin() { + this.sendAction('emailLogin'); + }, + + externalLogin(provider) { + this.sendAction('externalLogin', provider); } } }); diff --git a/app/assets/javascripts/discourse/components/login-modal.js.es6 b/app/assets/javascripts/discourse/components/login-modal.js.es6 index e366392b342..c4d710966ac 100644 --- a/app/assets/javascripts/discourse/components/login-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/login-modal.js.es6 @@ -11,7 +11,7 @@ export default Ember.Component.extend({ } Ember.run.schedule('afterRender', () => { - $('#login-account-password, #login-account-name').keydown(e => { + $('#login-account-password, #login-account-name, #login-second-factor').keydown(e => { if (e.keyCode === 13) { this.sendAction(); } diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index 888eb3c28ca..5c3b21d3b2e 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -3,9 +3,9 @@ import { bufferedRender } from 'discourse-common/lib/buffered-render'; export default Ember.Component.extend(bufferedRender({ tagName: 'li', - classNameBindings: ['active', 'content.hasIcon:has-icon', 'content.classNames'], + classNameBindings: ['active', 'content.hasIcon:has-icon', 'content.classNames', 'hidden'], attributeBindings: ['content.title:title'], - hidden: Em.computed.not('content.visible'), + hidden: false, rerenderTriggers: ['content.count'], @computed("content.filterMode", "filterMode") @@ -27,6 +27,12 @@ export default Ember.Component.extend(bufferedRender({ } } + if (!this.get('active') && this.currentUser && this.currentUser.trust_level > 0 && (content.get('name') === "new" || content.get('name') === "unread") && (content.get('count') < 1)) { + this.set('hidden', true); + } else { + this.set('hidden', false); + } + buffer.push(``); if (content.get('hasIcon')) { buffer.push(""); diff --git a/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6 b/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6 new file mode 100644 index 00000000000..2c2f5c1012c --- /dev/null +++ b/app/assets/javascripts/discourse/components/notification-consent-banner.js.es6 @@ -0,0 +1,45 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +import { + keyValueStore as pushNotificationKeyValueStore +} from 'discourse/lib/push-notifications'; + +import { default as DesktopNotificationConfig } from 'discourse/components/desktop-notification-config'; + +const userDismissedPromptKey = "dismissed-prompt"; + +export default DesktopNotificationConfig.extend({ + @computed + bannerDismissed: { + set(value) { + pushNotificationKeyValueStore.setItem(userDismissedPromptKey, value); + return pushNotificationKeyValueStore.getItem(userDismissedPromptKey); + }, + get() { + return pushNotificationKeyValueStore.getItem(userDismissedPromptKey); + } + }, + + @computed("isNotSupported", "isEnabled", "bannerDismissed", "currentUser.reply_count", "currentUser.topic_count") + showNotificationPromptBanner(isNotSupported, isEnabled, bannerDismissed, replyCount, topicCount) { + return (this.siteSettings.push_notifications_prompt && + !isNotSupported && + this.currentUser && + replyCount + topicCount > 0 && + Notification.permission !== "denied" && + Notification.permission !== "granted" && + !isEnabled && + !bannerDismissed + ); + }, + + actions: { + turnon() { + this._super(); + this.set("bannerDismissed", true); + }, + dismiss() { + this.set("bannerDismissed", true); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/period-chooser.js.es6 b/app/assets/javascripts/discourse/components/period-chooser.js.es6 deleted file mode 100644 index e3bc67fb5fe..00000000000 --- a/app/assets/javascripts/discourse/components/period-chooser.js.es6 +++ /dev/null @@ -1,46 +0,0 @@ -import CleansUp from 'discourse/mixins/cleans-up'; - -export default Ember.Component.extend(CleansUp, { - classNames: 'period-chooser', - showPeriods: false, - - cleanUp: function() { - this.set('showPeriods', false); - $('html').off('mousedown.top-period'); - }, - - _clickToClose: function() { - const self = this; - $('html').off('mousedown.top-period').on('mousedown.top-period', function(e) { - const $target = $(e.target); - if (($target.prop('id') === 'topic-entrance') || (self.$().has($target).length !== 0)) { - return; - } - self.cleanUp(); - }); - }, - - click(e) { - if ($(e.target).closest('.period-popup').length) { return; } - - if (!this.get('showPeriods')) { - if (!this.site.mobileView) { - const $chevron = this.$('.d-icon-caret-down'); - this.$('#period-popup').css($chevron.position()); - } else { - this.$('#period-popup').css({top: this.$().height()}); - } - this.set('showPeriods', true); - this._clickToClose(); - } - }, - - actions: { - changePeriod(p) { - this.cleanUp(); - this.set('period', p); - this.sendAction('action', p); - } - } - -}); diff --git a/app/assets/javascripts/discourse/components/quote-button.js.es6 b/app/assets/javascripts/discourse/components/quote-button.js.es6 index 5d41b049cc7..49ee520e95a 100644 --- a/app/assets/javascripts/discourse/components/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/components/quote-button.js.es6 @@ -78,7 +78,10 @@ export default Ember.Component.extend({ const $quoteButton = this.$(); // remove the marker - markerElement.parentNode.removeChild(markerElement); + const parent = markerElement.parentNode; + parent.removeChild(markerElement); + // merge back all text nodes so they don't get messed up + parent.normalize(); // work around Safari that would sometimes lose the selection if (isSafari) { diff --git a/app/assets/javascripts/discourse/components/radio-button.js.es6 b/app/assets/javascripts/discourse/components/radio-button.js.es6 index 3004f941068..252f7d7ac4c 100644 --- a/app/assets/javascripts/discourse/components/radio-button.js.es6 +++ b/app/assets/javascripts/discourse/components/radio-button.js.es6 @@ -5,8 +5,12 @@ export default Ember.Component.extend({ type : "radio", attributeBindings : ["name", "type", "value", "checked:checked", "disabled:disabled"], - click: function() { - this.set("selection", this.$().val()); + click() { + const value = this.$().val(); + if (this.get("selection") === value) { + this.set("selection", undefined); + } + this.set("selection", value); }, @computed('value', 'selection') diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 index b36b39a7019..79acf111840 100644 --- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 +++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 @@ -54,8 +54,7 @@ export default MountWidget.extend({ const scrollTop = $body.scrollTop(); // This hack is for when swapping out many cloaked views at once - // when using keyboard navigation. It could suddenly move the - // scroll + // when using keyboard navigation. It could suddenly move the scroll if (this.prevHeight === height && scrollTop !== this.prevScrollTop) { $body.scrollTop(this.prevScrollTop); } @@ -80,7 +79,7 @@ export default MountWidget.extend({ const postsWrapperTop = $('.posts-wrapper').offset().top; const $posts = this.$('.onscreen-post, .cloaked-post'); const viewportTop = windowTop - slack; - const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length-1); + const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length - 1); let windowBottom = windowTop + windowHeight; let viewportBottom = windowBottom + slack; @@ -93,10 +92,17 @@ export default MountWidget.extend({ let percent = null; const offset = offsetCalculator(); - const topCheck = Math.ceil(windowTop + offset); + const topCheck = Math.ceil(windowTop + offset + 5); // uncomment to debug the eyeline - // $('.debug-eyeline').css({ height: '1px', width: '100%', backgroundColor: 'blue', position: 'absolute', top: `${topCheck}px` }); + /* + let $eyeline = $('.debug-eyeline'); + if ($eyeline.length === 0) { + $('body').prepend('
      '); + $eyeline = $('.debug-eyeline'); + } + $eyeline.css({ height: '5px', width: '100%', backgroundColor: 'blue', position: 'absolute', top: `${topCheck}px`, zIndex: 999999 }); + */ let allAbove = true; let bottomView = topView; @@ -108,7 +114,7 @@ export default MountWidget.extend({ if (!$post) { break; } const viewTop = $post.offset().top; - const postHeight = $post.height(); + const postHeight = $post.outerHeight(true); const viewBottom = Math.ceil(viewTop + postHeight); allAbove = allAbove && (viewTop < topCheck); @@ -163,7 +169,7 @@ export default MountWidget.extend({ this.sendAction('topVisibleChanged', { post: first, refresh: topRefresh }); } - const last = posts.objectAt(onscreen[onscreen.length-1]); + const last = posts.objectAt(onscreen[onscreen.length - 1]); if (this._bottomVisible !== last) { this._bottomVisible = last; this.sendAction('bottomVisibleChanged', { post: last, refresh }); @@ -177,7 +183,7 @@ export default MountWidget.extend({ } if (percent !== null) { - if (percent > 1.0) { percent = 1.0; } + percent = Math.max(0.0, Math.min(1.0, percent)); if (changedPost || (this._currentPercent !== percent)) { this._currentPercent = percent; diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index a171083523d..22976d76d34 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -18,6 +18,7 @@ const REGEXP_TAGS_REPLACE = /(^(tags?:|#(?=[a-z0-9\-]+::tag))|::tag\s?$ const REGEXP_IN_MATCH = /^(in|with):(posted|watching|tracking|bookmarks|first|pinned|unpinned|wiki|unseen|image)/ig; const REGEXP_SPECIAL_IN_LIKES_MATCH = /^in:likes/ig; +const REGEXP_SPECIAL_IN_TITLE_MATCH = /^in:title/ig; const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /^in:private/ig; const REGEXP_SPECIAL_IN_SEEN_MATCH = /^in:seen/ig; @@ -81,6 +82,7 @@ export default Em.Component.extend({ in: '', special: { in: { + title: false, likes: false, private: false, seen: false @@ -111,6 +113,7 @@ export default Em.Component.extend({ this.setSearchedTermValueForTags(); this.setSearchedTermValue('searchedTerms.in', REGEXP_IN_PREFIX, REGEXP_IN_MATCH); this.setSearchedTermSpecialInValue('searchedTerms.special.in.likes', REGEXP_SPECIAL_IN_LIKES_MATCH); + this.setSearchedTermSpecialInValue('searchedTerms.special.in.title', REGEXP_SPECIAL_IN_TITLE_MATCH); this.setSearchedTermSpecialInValue('searchedTerms.special.in.private', REGEXP_SPECIAL_IN_PRIVATE_MATCH); this.setSearchedTermSpecialInValue('searchedTerms.special.in.seen', REGEXP_SPECIAL_IN_SEEN_MATCH); this.setSearchedTermValue('searchedTerms.status', REGEXP_STATUS_PREFIX); @@ -424,15 +427,14 @@ export default Em.Component.extend({ } }, - @observes('searchedTerms.special.in.likes') - updateSearchTermForSpecialInLikes() { - const match = this.filterBlocks(REGEXP_SPECIAL_IN_LIKES_MATCH); - const inFilter = this.get('searchedTerms.special.in.likes'); + updateInRegex(regex, filter) { + const match = this.filterBlocks(regex); + const inFilter = this.get('searchedTerms.special.in.' + filter); let searchTerm = this.get('searchTerm') || ''; if (inFilter) { if (match.length === 0) { - searchTerm += ` in:likes`; + searchTerm += ` in:${filter}`; this.set('searchTerm', searchTerm.trim()); } } else if (match.length !== 0) { @@ -441,38 +443,24 @@ export default Em.Component.extend({ } }, + @observes('searchedTerms.special.in.likes') + updateSearchTermForSpecialInLikes() { + this.updateInRegex(REGEXP_SPECIAL_IN_LIKES_MATCH, 'likes'); + }, + @observes('searchedTerms.special.in.private') updateSearchTermForSpecialInPrivate() { - const match = this.filterBlocks(REGEXP_SPECIAL_IN_PRIVATE_MATCH); - const inFilter = this.get('searchedTerms.special.in.private'); - let searchTerm = this.get('searchTerm') || ''; - - if (inFilter) { - if (match.length === 0) { - searchTerm += ` in:private`; - this.set('searchTerm', searchTerm.trim()); - } - } else if (match.length !== 0) { - searchTerm = searchTerm.replace(match, ''); - this.set('searchTerm', searchTerm.trim()); - } + this.updateInRegex(REGEXP_SPECIAL_IN_PRIVATE_MATCH, 'private'); }, @observes('searchedTerms.special.in.seen') updateSearchTermForSpecialInSeen() { - const match = this.filterBlocks(REGEXP_SPECIAL_IN_SEEN_MATCH); - const inFilter = this.get('searchedTerms.special.in.seen'); - let searchTerm = this.get('searchTerm') || ''; + this.updateInRegex(REGEXP_SPECIAL_IN_SEEN_MATCH, 'seen'); + }, - if (inFilter) { - if (match.length === 0) { - searchTerm += ` in:seen`; - this.set('searchTerm', searchTerm.trim()); - } - } else if (match.length !== 0) { - searchTerm = searchTerm.replace(match, ''); - this.set('searchTerm', searchTerm.trim()); - } + @observes('searchedTerms.special.in.title') + updateSearchTermForSpecialInTitle() { + this.updateInRegex(REGEXP_SPECIAL_IN_TITLE_MATCH, 'title'); }, @observes('searchedTerms.status') diff --git a/app/assets/javascripts/discourse/components/search-text-field.js.es6 b/app/assets/javascripts/discourse/components/search-text-field.js.es6 index 85547425795..7e449b35700 100644 --- a/app/assets/javascripts/discourse/components/search-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -6,7 +6,7 @@ import { applySearchAutocomplete } from "discourse/lib/search"; export default TextField.extend({ @computed('searchService.searchContextEnabled') placeholder(searchContextEnabled) { - return searchContextEnabled ? "" : I18n.t('search.title'); + return searchContextEnabled ? "" : I18n.t('search.full_page_title'); }, @on("didInsertElement") diff --git a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 new file mode 100644 index 00000000000..0fb51515c3b --- /dev/null +++ b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 @@ -0,0 +1,32 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + tagName: '', + publishing: false, + + @computed('topic.destination_category_id') + validCategory(destCatId) { + return destCatId && + destCatId !== this.site.shared_drafts_category_id; + }, + + actions: { + updateDestinationCategory(category) { + return this.get('topic').updateDestinationCategory(category.get('id')); + }, + + publish() { + bootbox.confirm(I18n.t('shared_drafts.confirm_publish'), result => { + if (result) { + this.set('publishing', true); + let destId = this.get('topic.destination_category_id'); + this.get('topic').publish().then(() => { + this.set('topic.category_id', destId); + }).finally(() => { + this.set('publishing', false); + }); + } + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 index 56731c96df4..ac19edd63dd 100644 --- a/app/assets/javascripts/discourse/components/site-header.js.es6 +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -16,7 +16,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { _topic: null, @observes('currentUser.unread_notifications', 'currentUser.unread_private_messages') - _notificationsChanged() { + notificationsChanged() { this.queueRerender(); }, @@ -112,7 +112,8 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { $panel.removeClass('drop-down').removeClass('slide-in').addClass(viewMode); const $panelBody = $('.panel-body', $panel); - let contentHeight = parseInt($('.panel-body-contents', $panel).height()); + // 2 pixel fudge allows for firefox subpixel sizing stuff causing scrollbar + let contentHeight = parseInt($('.panel-body-contents', $panel).height()) + 2; // We use a mutationObserver to check for style changes, so it's important // we don't set it if it doesn't change. Same goes for the $panelBody! @@ -139,7 +140,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { if ($panelBody.height() !== contentHeight) { $panelBody.height(contentHeight); } - $('body').addClass('drop-down-visible'); + $('body').addClass('drop-down-mode'); } else { const menuTop = headerHeight(); @@ -157,7 +158,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { if (style.top !== menuTop + "px" || style.height !== height) { $panel.css({ top: menuTop + "px", height }); } - $('body').removeClass('drop-down-visible'); + $('body').removeClass('drop-down-mode'); } $panel.width(width); diff --git a/app/assets/javascripts/discourse/components/suggested-topics.js.es6 b/app/assets/javascripts/discourse/components/suggested-topics.js.es6 index 87ffd1865b6..b557bda31a0 100644 --- a/app/assets/javascripts/discourse/components/suggested-topics.js.es6 +++ b/app/assets/javascripts/discourse/components/suggested-topics.js.es6 @@ -7,8 +7,9 @@ export default Ember.Component.extend({ @computed('topic') suggestedTitle(topic) { - return topic.get('isPrivateMessage') ? - `
      ${iconHTML('envelope', { class: 'private-message-glyph' })} ${I18n.t("suggested_topics.pm_title")}` : + const href = this.currentUser && this.currentUser.pmPath(topic); + return topic.get('isPrivateMessage') && href ? + `${iconHTML('envelope', { class: 'private-message-glyph' })}${I18n.t("suggested_topics.pm_title")}` : I18n.t("suggested_topics.title"); }, @@ -31,7 +32,7 @@ export default Ember.Component.extend({ } const unreadTopics = this.topicTrackingState.countUnread(); - const newTopics = this.topicTrackingState.countNew(); + const newTopics = this.currentUser ? this.topicTrackingState.countNew() : 0; if (newTopics + unreadTopics > 0) { const hasBoth = unreadTopics > 0 && newTopics > 0; diff --git a/app/assets/javascripts/discourse/components/tag-chooser.js.es6 b/app/assets/javascripts/discourse/components/tag-chooser.js.es6 deleted file mode 100644 index 00fc2444879..00000000000 --- a/app/assets/javascripts/discourse/components/tag-chooser.js.es6 +++ /dev/null @@ -1,139 +0,0 @@ -import renderTag from 'discourse/lib/render-tag'; - -function formatTag(t) { - return renderTag(t.id, {count: t.count, noHref: true}); -} - -export default Ember.TextField.extend({ - classNameBindings: [':tag-chooser'], - attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'], - - init() { - this._super(); - const tags = this.get('tags') || []; - this.set('value', tags.join(", ")); - - if (this.get('allowCreate') !== false) { - this.set('allowCreate', this.site.get('can_create_tag')); - } - - this.set('termMatchesForbidden', false); - }, - - _valueChanged: function() { - const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq(); - this.set('tags', tags); - }.observes('value'), - - _tagsChanged: function() { - const $tagChooser = this.$(), - val = this.get('value'); - - if ($tagChooser && val !== this.get('tags')) { - if (this.get('tags')) { - const data = this.get('tags').map((t) => {return {id: t, text: t};}); - $tagChooser.select2('data', data); - } else { - $tagChooser.select2('data', []); - } - } - }.observes('tags'), - - didInsertElement() { - this._super(); - - const self = this; - const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g"); - - let limit = this.siteSettings.max_tags_per_topic; - - if (this.get('unlimitedTagCount')) { - limit = null; - } else if (this.get('limit')) { - limit = parseInt(this.get('limit')); - } - - this.$().select2({ - tags: true, - placeholder: this.get('placeholder') === "" ? "" : I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'), - maximumInputLength: this.siteSettings.max_tag_length, - maximumSelectionSize: limit, - width: this.get('width') || 'resolve', - initSelection(element, callback) { - const data = []; - - function splitVal(string, separator) { - var val, i, l; - if (string === null || string.length < 1) return []; - val = string.split(separator); - for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); - return val; - } - - $(splitVal(element.val(), ",")).each(function () { - data.push({ - id: this, - text: this - }); - }); - - callback(data); - }, - createSearchChoice(term, data) { - term = term.replace(filterRegexp, '').trim().toLowerCase(); - - // No empty terms, make sure the user has permission to create the tag - if (!term.length || !self.get('allowCreate') || self.get('termMatchesForbidden')) return; - - if ($(data).filter(function() { - return this.text.localeCompare(term) === 0; - }).length === 0) { - return { id: term, text: term }; - } - }, - createSearchChoicePosition(list, item) { - // Search term goes on the bottom - list.push(item); - }, - formatSelection(data) { - return data ? renderTag(this.text(data), {noHref: true}) : undefined; - }, - formatSelectionCssClass() { - return "discourse-tag-select2"; - }, - formatResult: formatTag, - multiple: true, - ajax: { - quietMillis: 200, - cache: true, - url: Discourse.getURL("/tags/filter/search"), - dataType: 'json', - data: function (term) { - const d = { - q: term, - limit: self.siteSettings.max_tag_search_results, - categoryId: self.get('categoryId'), - selected_tags: self.get('tags') - }; - if (!self.get('everyTag')) { - d.filterForInput = true; - } - return d; - }, - results: function (data) { - if (self.siteSettings.tags_sort_alphabetically) { - data.results = data.results.sort(function(a,b) { return a.id > b.id; }); - } - self.set('termMatchesForbidden', data.forbidden ? true : false); - return data; - } - }, - }); - }, - - willDestroyElement() { - this._super(); - this.$().select2('destroy'); - } - -}); diff --git a/app/assets/javascripts/discourse/components/tag-drop.js.es6 b/app/assets/javascripts/discourse/components/tag-drop.js.es6 deleted file mode 100644 index 201d9a134d5..00000000000 --- a/app/assets/javascripts/discourse/components/tag-drop.js.es6 +++ /dev/null @@ -1,139 +0,0 @@ -import { setting } from 'discourse/lib/computed'; -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Component.extend({ - classNameBindings: [':tag-drop', 'tag::no-category', 'tags:has-drop','categoryStyle','tagClass'], - categoryStyle: setting('category_style'), // match the category-drop style - currentCategory: Em.computed.or('secondCategory', 'firstCategory'), - showFilterByTag: setting('show_filter_by_tag'), - showTagDropdown: Em.computed.and('showFilterByTag', 'tags'), - tagId: null, - - tagName: 'li', - - @computed('site.top_tags') - tags(topTags) { - if (this.siteSettings.tags_sort_alphabetically && topTags) { - return topTags.sort(); - } else { - return topTags; - } - }, - - @computed('expanded') - expandedIcon(expanded) { - return expanded ? 'd-drop-expanded' : 'd-drop-collapsed'; - }, - - @computed('tagId') - tagClass() { - if (this.get('tagId')) { - return "tag-" + this.get('tagId'); - } else { - return "tag_all"; - } - }, - - @computed('firstCategory', 'secondCategory') - allTagsUrl() { - if (this.get('currentCategory')) { - return this.get('currentCategory.url') + "?allTags=1"; - } else { - return "/"; - } - }, - - @computed('tag') - allTagsLabel() { - return I18n.t("tagging.selector_all_tags"); - }, - - @computed('tagId') - noTagsSelected() { - return this.get('tagId') === 'none'; - }, - - @computed('firstCategory', 'secondCategory') - noTagsUrl() { - var url = '/tags'; - if (this.get('currentCategory')) { - url += this.get('currentCategory.url'); - } - return url + '/none'; - }, - - @computed('tag') - noTagsLabel() { - return I18n.t("tagging.selector_no_tags"); - }, - - @computed('tag') - dropdownButtonClass() { - let result = 'dropdown-header category-dropdown-button'; - if (Em.isNone(this.get('tag'))) { - result += ' home'; - } - return result; - }, - - @computed('tag') - clickEventName() { - return "click.tag-drop-" + (this.get('tag') || "all"); - }, - - actions: { - expand: function() { - var self = this; - - if(!this.get('renderTags')){ - this.set('renderTags',true); - Em.run.next(function(){ - self.send('expand'); - }); - return; - } - - if (this.get('expanded')) { - this.close(); - return; - } - - if (this.get('tags')) { - this.set('expanded', true); - } - var $dropdown = this.$()[0]; - - this.$('a[data-drop-close]').on('click.tag-drop', function() { - self.close(); - }); - - Em.run.next(function(){ - self.$('.cat a').add('html').on(self.get('clickEventName'), function(e) { - var $target = $(e.target), - closest = $target.closest($dropdown); - - if ($(e.currentTarget).hasClass('badge-wrapper')){ - self.close(); - } - - return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close(); - }); - }); - } - }, - - removeEvents: function(){ - $('html').off(this.get('clickEventName')); - this.$('a[data-drop-close]').off('click.tag-drop'); - }, - - close: function() { - this.removeEvents(); - this.set('expanded', false); - }, - - willDestroyElement: function() { - this.removeEvents(); - } - -}); diff --git a/app/assets/javascripts/discourse/components/tag-group-chooser.js.es6 b/app/assets/javascripts/discourse/components/tag-group-chooser.js.es6 deleted file mode 100644 index efe35db5bb5..00000000000 --- a/app/assets/javascripts/discourse/components/tag-group-chooser.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -function renderTagGroup(tag) { - return "" + Handlebars.Utils.escapeExpression(tag.text ? tag.text : tag) + ""; -}; - -export default Ember.TextField.extend({ - classNameBindings: [':tag-chooser'], - attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'], - - _initValue: function() { - const names = this.get('tagGroups') || []; - this.set('value', names.join(",")); - }.on('init'), - - _valueChanged: function() { - const names = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq(); - if ( this.get('tagGroups').join(',') !== this.get('value') ) { - this.set('tagGroups', names); - } - }.observes('value'), - - _tagGroupsChanged: function() { - const $chooser = this.$(), - val = this.get('value'); - - if ($chooser && val !== this.get('tagGroups')) { - if (this.get('tagGroups')) { - const data = this.get('tagGroups').map((t) => {return {id: t, text: t};}); - $chooser.select2('data', data); - } else { - $chooser.select2('data', []); - } - } - }.observes('tagGroups'), - - _initializeChooser: function() { - const self = this; - - this.$().select2({ - tags: true, - placeholder: this.get('placeholderKey') ? I18n.t(this.get('placeholderKey')) : null, - initSelection(element, callback) { - const data = []; - - function splitVal(string, separator) { - var val, i, l; - if (string === null || string.length < 1) return []; - val = string.split(separator); - for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]); - return val; - } - - $(splitVal(element.val(), ",")).each(function () { - data.push({ id: this, text: this }); - }); - - callback(data); - }, - formatSelection: function (data) { - return data ? renderTagGroup(this.text(data)) : undefined; - }, - formatSelectionCssClass: function(){ - return "discourse-tag-select2"; - }, - formatResult: renderTagGroup, - multiple: true, - ajax: { - quietMillis: 200, - cache: true, - url: Discourse.getURL("/tag_groups/filter/search"), - dataType: 'json', - data: function (term) { - return { q: term, limit: self.siteSettings.max_tag_search_results }; - }, - results: function (data) { - data.results = data.results.sort(function(a,b) { return a.text > b.text; }); - return data; - } - }, - }); - }.on('didInsertElement'), - - _destroyChooser: function() { - this.$().select2('destroy'); - }.on('willDestroyElement') - -}); diff --git a/app/assets/javascripts/discourse/components/tag-list.js.es6 b/app/assets/javascripts/discourse/components/tag-list.js.es6 index df2c024cb87..decb1cea5df 100644 --- a/app/assets/javascripts/discourse/components/tag-list.js.es6 +++ b/app/assets/javascripts/discourse/components/tag-list.js.es6 @@ -1,21 +1,23 @@ +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ classNameBindings: [':tag-list', 'categoryClass'], + isPrivateMessage: false, sortedTags: Ember.computed.sort('tags', 'sortProperties'), - title: function() { - if (this.get('titleKey')) { return I18n.t(this.get('titleKey')); } - }.property('titleKey'), + @computed("titleKey") + title(titleKey) { + return titleKey && I18n.t(titleKey); + }, - category: function() { - if (this.get('categoryId')) { - return Discourse.Category.findById(this.get('categoryId')); - } - }.property('categoryId'), + @computed("categoryId") + category(categoryId) { + return categoryId && Discourse.Category.findById(categoryId); + }, - categoryClass: function() { - if (this.get('category')) { - return "tag-list-" + this.get('category.fullSlug'); - } - }.property('category') + @computed("category.fullSlug") + categoryClass(slug) { + return slug && `tag-list-${slug}`; + } }); diff --git a/app/assets/javascripts/discourse/components/text-field.js.es6 b/app/assets/javascripts/discourse/components/text-field.js.es6 index a9246efa7db..6e1fabc8195 100644 --- a/app/assets/javascripts/discourse/components/text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/text-field.js.es6 @@ -1,7 +1,35 @@ import computed from "ember-addons/ember-computed-decorators"; +import { siteDir, isRTL, isLTR } from "discourse/lib/text-direction"; export default Ember.TextField.extend({ - attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'], + attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength', 'dir'], + + @computed + dir() { + if (this.siteSettings.support_mixed_text_direction) { + let val = this.value; + if (val) { + return isRTL(val) ? 'rtl' : 'ltr'; + } else { + return siteDir(); + } + } + }, + + keyUp(event) { + this._super(event); + + if (this.siteSettings.support_mixed_text_direction) { + let val = this.value; + if (isRTL(val)) { + this.set('dir', 'rtl'); + } else if (isLTR(val)) { + this.set('dir', 'ltr'); + } else { + this.set('dir', siteDir()); + } + } + }, @computed("placeholderKey") placeholder(placeholderKey) { diff --git a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 index e370f0d3379..d2c110adfd8 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 @@ -6,6 +6,16 @@ export default Ember.Component.extend({ // Allow us to extend it layoutName: 'components/topic-footer-buttons', + @computed('topic.isPrivateMessage') + canArchive(isPM) { + return this.siteSettings.enable_personal_messages && isPM; + }, + + @computed('topic.isPrivateMessage') + showNotificationsButton(isPM) { + return (!isPM) || this.siteSettings.enable_personal_messages; + }, + @computed('topic.details.can_invite_to') canInviteTo(result) { return !this.site.mobileView && result; @@ -18,6 +28,11 @@ export default Ember.Component.extend({ return !this.site.mobileView && this.currentUser && this.currentUser.get('canManageTopic'); }, + showEditOnFooter: Ember.computed.and( + 'topic.isPrivateMessage', + 'site.can_tag_pms' + ), + @computed('topic.message_archived') archiveIcon: archived => archived ? '' : 'folder', diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 index f9b3cd38b76..b521ea876bf 100644 --- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 @@ -33,6 +33,11 @@ export default Ember.Component.extend(bufferedRender({ attributeBindings: ['data-topic-id'], 'data-topic-id': Em.computed.alias('topic.id'), + @computed + newDotText() { + return (this.currentUser && this.currentUser.trust_level > 0) ? "" : I18n.t('filters.new.lower_title'); + }, + actions: { toggleBookmark() { this.get('topic').toggleBookmark().finally(() => this.rerenderBuffer()); @@ -58,8 +63,15 @@ export default Ember.Component.extend(bufferedRender({ classes.push('has-excerpt'); } + if (topic.get('unseen')) { + classes.push("unseen-topic"); + } - ['liked', 'archived', 'bookmarked', 'pinned'].forEach(name => { + if (topic.get('displayNewPosts')) { + classes.push("new-posts"); + } + + ['liked', 'archived', 'bookmarked', 'pinned', 'closed'].forEach(name => { if (topic.get(name)) { classes.push(name); } diff --git a/app/assets/javascripts/discourse/components/topic-list.js.es6 b/app/assets/javascripts/discourse/components/topic-list.js.es6 index 7dde582385e..bff2c380a60 100644 --- a/app/assets/javascripts/discourse/components/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list.js.es6 @@ -4,6 +4,7 @@ export default Ember.Component.extend({ tagName: 'table', classNames: ['topic-list'], showTopicPostBadges: true, + listTitle: 'topic.title', // Overwrite this to perform client side filtering of topics, if desired filteredTopics: Ember.computed.alias('topics'), diff --git a/app/assets/javascripts/discourse/components/topic-post-badges.js.es6 b/app/assets/javascripts/discourse/components/topic-post-badges.js.es6 index 9eb07ade4fb..f125c839b22 100644 --- a/app/assets/javascripts/discourse/components/topic-post-badges.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-post-badges.js.es6 @@ -13,9 +13,10 @@ export default Ember.Component.extend(bufferedRender({ rerenderTriggers: ['url', 'unread', 'newPosts', 'unseen'], buildBuffer(buffer) { + const newDotText = (this.currentUser && this.currentUser.trust_level > 0) ? " " : I18n.t('filters.new.lower_title'); const url = this.get('url'); link(buffer, this.get('unread'), url, 'unread', 'unread_posts'); link(buffer, this.get('newPosts'), url, 'new-posts', 'new_posts'); - link(buffer, this.get('unseen'), url, 'new-topic', 'new', I18n.t('filters.new.lower_title')); + link(buffer, this.get('unseen'), url, 'new-topic', 'new', newDotText); } })); diff --git a/app/assets/javascripts/discourse/components/topic-progress.js.es6 b/app/assets/javascripts/discourse/components/topic-progress.js.es6 index a3f09e3182a..70dcf5dccca 100644 --- a/app/assets/javascripts/discourse/components/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-progress.js.es6 @@ -111,47 +111,36 @@ export default Ember.Component.extend({ const style = `border-right-width: ${borderSize}; width: ${progressWidth}px`; $topicProgress.append(`
       
      `); } else { - $bg.css("border-right-width", borderSize).width(progressWidth); + $bg.css("border-right-width", borderSize).width(progressWidth - 2); } }, _dock() { - const maximumOffset = $('#topic-bottom').offset(), - composerHeight = $('#reply-control').height() || 0, - $topicProgressWrapper = this.$(), - offset = window.pageYOffset || $('html').scrollTop(); + const $wrapper = this.$(); + if (!$wrapper || $wrapper.length === 0) return; - if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) { - return; - } + const offset = window.pageYOffset || $("html").scrollTop(); + const progressHeight = this.site.mobileView ? 0 : $("#topic-progress").height(); + const maximumOffset = $("#topic-bottom").offset().top + progressHeight; + const windowHeight = $(window).height(); + const composerHeight = $("#reply-control").height() || 0; + const isDocked = offset >= maximumOffset - windowHeight + composerHeight; + const bottom = $("#main").height() - maximumOffset; - let isDocked = false; - if (maximumOffset) { - const threshold = maximumOffset.top; - const windowHeight = $(window).height(); - const headerHeight = $('header').outerHeight(true); - - if (this.capabilities.isIOS) { - isDocked = offset >= (threshold - windowHeight - headerHeight + composerHeight); - } else { - isDocked = offset >= (threshold - windowHeight + composerHeight); - } - } - - const dockPos = $(document).height() - $('#topic-bottom').offset().top; if (composerHeight > 0) { - if (isDocked) { - $topicProgressWrapper.css('bottom', dockPos); - } else { - const height = composerHeight + "px"; - if ($topicProgressWrapper.css('bottom') !== height) { - $topicProgressWrapper.css('bottom', height); - } - } + $wrapper.css("bottom", isDocked ? bottom : composerHeight); } else { - $topicProgressWrapper.css('bottom', isDocked ? dockPos : ''); + $wrapper.css("bottom", isDocked ? bottom : ""); + } + + this.set("docked", isDocked); + + const $replyArea = $("#reply-control .reply-area"); + if ($replyArea && $replyArea.length > 0) { + $wrapper.css("right", `${$replyArea.offset().left}px`); + } else { + $wrapper.css("right", "1em"); } - this.set('docked', isDocked); }, click(e) { @@ -160,7 +149,6 @@ export default Ember.Component.extend({ } }, - actions: { toggleExpansion() { this.toggleProperty('expanded'); diff --git a/app/assets/javascripts/discourse/components/topic-status.js.es6 b/app/assets/javascripts/discourse/components/topic-status.js.es6 index f1c8695dfde..be8076458e9 100644 --- a/app/assets/javascripts/discourse/components/topic-status.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-status.js.es6 @@ -8,15 +8,10 @@ export default Ember.Component.extend(bufferedRender({ rerenderTriggers: ['topic.archived', 'topic.closed', 'topic.pinned', 'topic.visible', 'topic.unpinned', 'topic.is_warning'], click(e) { - if ($(e.target).hasClass('d-icon-thumb-tack')) { + // only pin unpin for now + if (this.get("canAct") && $(e.target).hasClass('d-icon-thumb-tack')) { const topic = this.get('topic'); - - // only pin unpin for now - if (topic.get('pinned')) { - topic.clearPin(); - } else { - topic.rePin(); - } + topic.get('pinned') ? topic.clearPin() : topic.rePin(); } return false; diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 index c693fd9c942..747bc975655 100644 --- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 @@ -62,13 +62,14 @@ export default MountWidget.extend(Docking, { this.dockBottom = false; if (posTop < topicTop) { - this.dockAt = topicTop; + this.dockAt = parseInt(topicTop, 10); } else if (pos > topicBottom + footerHeight) { - this.dockAt = (topicBottom - timelineHeight) + footerHeight; + this.dockAt = parseInt((topicBottom - timelineHeight) + footerHeight, 10); this.dockBottom = true; if (this.dockAt < 0) { this.dockAt = 0; } } else { this.dockAt = null; + this.fastDockAt = parseInt(topicBottom - timelineHeight + footerHeight - offsetTop, 10); } if (this.dockAt !== prev) { diff --git a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 index 16ea7518340..e0e8fb91420 100644 --- a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 @@ -18,7 +18,6 @@ export default Ember.Component.extend(bufferedRender({ if (!this.get('executeAt')) return; let statusUpdateAt = moment(this.get('executeAt')); - if (statusUpdateAt < new Date()) return; let duration = moment.duration(statusUpdateAt - moment()); let minutesLeft = duration.asMinutes(); @@ -54,7 +53,7 @@ export default Ember.Component.extend(bufferedRender({ }, options); } - buffer.push(I18n.t(this._noticeKey(), options)); + buffer.push(`${I18n.t(this._noticeKey(), options)}`); buffer.push(''); // TODO Sam: concerned this can cause a heavy rerender loop diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 index b7d3b4d31fe..d75bab921a0 100644 --- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -1,45 +1,30 @@ -import { wantsNewWindow } from 'discourse/lib/intercept-click'; -import { propertyNotEqual, setting } from 'discourse/lib/computed'; -import CleansUp from 'discourse/mixins/cleans-up'; -import afterTransition from 'discourse/lib/after-transition'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -import DiscourseURL from 'discourse/lib/url'; import User from 'discourse/models/user'; -import { userPath } from 'discourse/lib/url'; +import { propertyNotEqual, setting } from 'discourse/lib/computed'; import { durationTiny } from 'discourse/lib/formatter'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; +import CardContentsBase from 'discourse/mixins/card-contents-base'; +import CleansUp from 'discourse/mixins/cleans-up'; -const clickOutsideEventName = "mousedown.outside-user-card"; -const clickDataExpand = "click.discourse-user-card"; -const clickMention = "click.discourse-user-mention"; - -export default Ember.Component.extend(CleansUp, CanCheckEmails, { +export default Ember.Component.extend(CardContentsBase, CanCheckEmails, CleansUp, { elementId: 'user-card', - classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'], + triggeringLinkClass: 'mention', + classNameBindings: ['visible:show', 'showBadges', 'user.card_background::no-bg', 'isFixed:fixed'], allowBackgrounds: setting('allow_profile_backgrounds'), + showBadges: setting('enable_badges'), postStream: Ember.computed.alias('topic.postStream'), enoughPostsForFiltering: Ember.computed.gte('topicPostCount', 2), - viewingTopic: Ember.computed.match('currentPath', /^topic\./), - viewingAdmin: Ember.computed.match('currentPath', /^admin\./), showFilter: Ember.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'), showName: propertyNotEqual('user.name', 'user.username'), hasUserFilters: Ember.computed.gt('postStream.userFilters.length', 0), - isSuspended: Ember.computed.notEmpty('user.suspend_reason'), - showBadges: setting('enable_badges'), showMoreBadges: Ember.computed.gt('moreBadgesCount', 0), showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"), linkWebsite: Ember.computed.not('user.isBasic'), hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'), showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'), - visible: false, user: null, - username: null, - avatar: null, - userLoading: null, - cardTarget: null, - post: null, // If inside a topic topicPostCount: null, @@ -75,21 +60,6 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, { @computed('user.badge_count', 'user.featured_user_badges.length') moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength, - @computed('user.card_badge.image') - hasCardBadgeImage: image => image && image.indexOf('fa-') !== 0, - - @observes('user.card_background') - addBackground() { - if (!this.get('allowBackgrounds')) { return; } - - const $this = this.$(); - if (!$this) { return; } - - const url = this.get('user.card_background'); - const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`; - $this.css('background-image', bg); - }, - @computed('user.time_read', 'user.recent_time_read') showRecentTimeRead(timeRead, recentTimeRead) { return timeRead !== recentTimeRead && recentTimeRead !== 0; @@ -109,144 +79,40 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, { } }, - _show(username, $target) { - // No user card for anon - if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { - return false; - } + @observes('user.card_background') + addBackground() { + if (!this.get('allowBackgrounds')) { return; } - username = Ember.Handlebars.Utils.escapeExpression(username.toString()); + const $this = this.$(); + if (!$this) { return; } - // Don't show on mobile - if (this.site.mobileView) { - DiscourseURL.routeTo(userPath(username)); - return false; - } - - const currentUsername = this.get('username'); - if (username === currentUsername && this.get('userLoading') === username) { - return; - } - - const postId = $target.parents('article').data('post-id'); - - const wasVisible = this.get('visible'); - const previousTarget = this.get('cardTarget'); - const target = $target[0]; - if (wasVisible) { - this._close(); - if (target === previousTarget) { return; } - } - - const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; - this.setProperties({ username, userLoading: username, cardTarget: target, post }); + const url = this.get('user.card_background'); + const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`; + $this.css('background-image', bg); + }, + _showCallback(username, $target) { const args = { stats: false }; args.include_post_count_for = this.get('topic.id'); - User.findByUsername(username, args).then(user => { if (user.topic_post_count) { this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]); } - this.setProperties({ user, avatar: user, visible: true }); - this._positionCard($target); - }).catch(() => this._close()).finally(() => this.set('userLoading', null)); + this.setProperties({ user, visible: true }); - return false; + }).catch(() => this._close()).finally(() => this.set('loading', null)); }, didInsertElement() { this._super(); - afterTransition(this.$(), this._hide.bind(this)); - - $('html').off(clickOutsideEventName) - .on(clickOutsideEventName, (e) => { - if (this.get('visible')) { - const $target = $(e.target); - if ($target.closest('[data-user-card]').data('userCard') || - $target.closest('a.mention').length > 0 || - $target.closest('#user-card').length > 0) { - return; - } - - this._close(); - } - - return true; - }); - - $('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => { - if (wantsNewWindow(e)) { return; } - const $target = $(e.currentTarget); - return this._show($target.data('user-card'), $target); - }); - - $('#main-outlet').on(clickMention, 'a.mention', (e) => { - if (wantsNewWindow(e)) { return; } - const $target = $(e.target); - return this._show($target.text().replace(/^@/, ''), $target); - }); - }, - - _positionCard(target) { - const rtl = ($('html').css('direction')) === 'rtl'; - if (!target) { return; } - const width = this.$().width(); - - Ember.run.schedule('afterRender', () => { - if (target) { - let position = target.offset(); - if (position) { - - if (rtl) { // The site direction is rtl - position.right = $(window).width() - position.left + 10; - position.left = 'auto'; - let overage = ($(window).width() - 50) - (position.right + width); - if (overage < 0) { - position.right += overage; - position.top += target.height() + 48; - } - } else { // The site direction is ltr - position.left += target.width() + 10; - - let overage = ($(window).width() - 50) - (position.left + width); - if (overage < 0) { - position.left += overage; - position.top += target.height() + 48; - } - } - - position.top -= $('#main-outlet').offset().top; - this.$().css(position); - } - - // After the card is shown, focus on the first link - // - // note: we DO NOT use afterRender here cause _positionCard may - // run afterwards, if we allowed this to happen the usercard - // may be offscreen and we may scroll all the way to it on focus - Ember.run.next(null, () => this.$('a:first').focus() ); - } - }); - }, - - _hide() { - if (!this.get('visible')) { - this.$().css({left: -9999, top: -9999}); - } }, _close() { + this._super(); this.setProperties({ - visible: false, user: null, - username: null, - avatar: null, - userLoading: null, - cardTarget: null, - post: null, - topicPostCount: null + topicPostCount: null, }); }, @@ -254,20 +120,6 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, { this._close(); }, - keyUp(e) { - if (e.keyCode === 27) { // ESC - const target = this.get('cardTarget'); - this._close(); - target.focus(); - } - }, - - willDestroyElement() { - this._super(); - $('html').off(clickOutsideEventName); - $('#main').off(clickDataExpand).off(clickMention); - }, - actions: { close() { this._close(); diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index 830a74d708c..561197075d3 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -16,20 +16,31 @@ export default TextField.extend({ didInsertElement(opts) { this._super(); + + const bool = (n => { + const val = this.get(n); + return val === true || val === "true"; + }); + var self = this, selected = [], groups = [], currentUser = this.currentUser, - includeMentionableGroups = this.get('includeMentionableGroups') === 'true', - includeMessageableGroups = this.get('includeMessageableGroups') === 'true', - includeGroups = this.get('includeGroups') === 'true', - allowedUsers = this.get('allowedUsers') === 'true'; + includeMentionableGroups = bool('includeMentionableGroups'), + includeMessageableGroups = bool('includeMessageableGroups'), + includeGroups = bool('includeGroups'), + allowedUsers = bool('allowedUsers'), + excludeCurrentUser = bool('excludeCurrentUser'), + single = bool('single'), + allowAny = bool('allowAny'), + disabled = bool('disabled'), + disallowEmails = bool('disallowEmails'); function excludedUsernames() { // hack works around some issues with allowAny eventing - const usernames = self.get('single') ? [] : selected; + const usernames = single ? [] : selected; - if (currentUser && self.get('excludeCurrentUser')) { + if (currentUser && excludeCurrentUser) { return usernames.concat([currentUser.get('username')]); } return usernames; @@ -37,9 +48,9 @@ export default TextField.extend({ this.$().val(this.get('usernames')).autocomplete({ template: findRawTemplate('user-selector-autocomplete'), - disabled: this.get('disabled'), - single: this.get('single'), - allowAny: this.get('allowAny'), + disabled: disabled, + single: single, + allowAny: allowAny, updateData: (opts && opts.updateData) ? opts.updateData : false, dataSource(term) { @@ -54,7 +65,8 @@ export default TextField.extend({ allowedUsers, includeMentionableGroups, includeMessageableGroups, - group: self.get("group") + group: self.get("group"), + disallowEmails, }); return results; diff --git a/app/assets/javascripts/discourse/components/user-stat.js.es6 b/app/assets/javascripts/discourse/components/user-stat.js.es6 index 8931501e3e6..933f11a11bd 100644 --- a/app/assets/javascripts/discourse/components/user-stat.js.es6 +++ b/app/assets/javascripts/discourse/components/user-stat.js.es6 @@ -1,5 +1,6 @@ export default Ember.Component.extend({ classNames: ['user-stat'], type: 'number', - isNumber: Ember.computed.equal('type', 'number') + isNumber: Ember.computed.equal('type', 'number'), + isDuration: Ember.computed.equal('type', 'duration') }); diff --git a/app/assets/javascripts/discourse/components/user-stream-item.js.es6 b/app/assets/javascripts/discourse/components/user-stream-item.js.es6 index 103f450b245..b239613c477 100644 --- a/app/assets/javascripts/discourse/components/user-stream-item.js.es6 +++ b/app/assets/javascripts/discourse/components/user-stream-item.js.es6 @@ -11,5 +11,5 @@ export default Ember.Component.extend({ ], moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"), - actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"), + actionDescription: actionDescription("item.action_code", "item.created_at", "item.action_code_who"), }); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index df41d58a0a9..bc64c7e3bfa 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -5,7 +5,7 @@ import Composer from 'discourse/models/composer'; import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; import InputValidation from 'discourse/models/input-validation'; import { getOwner } from 'discourse-common/lib/get-owner'; -import { escapeExpression } from 'discourse/lib/utilities'; +import { escapeExpression, authorizesOneOrMoreExtensions } from 'discourse/lib/utilities'; import { emojiUnescape } from 'discourse/lib/text'; import { shortDate } from 'discourse/lib/formatter'; @@ -41,7 +41,8 @@ function loadDraft(store, opts) { composerState: Composer.DRAFT, composerTime: draft.composerTime, typingTime: draft.typingTime, - whisper: draft.whisper + whisper: draft.whisper, + tags: draft.tags }); return composer; } @@ -70,6 +71,7 @@ export default Ember.Controller.extend({ scopedCategoryId: null, lastValidatedAt: null, isUploading: false, + allowUpload: false, topic: null, linkLookup: null, showPreview: true, @@ -139,7 +141,8 @@ export default Ember.Controller.extend({ return !this.site.mobileView && this.site.get('can_tag_topics') && canEditTitle && - !creatingPrivateMessage; + !creatingPrivateMessage && + (!this.get('model.topic.isPrivateMessage') || this.site.get('can_tag_pms')); }, @computed('model.whisper', 'model.unlistTopic') @@ -223,6 +226,11 @@ export default Ember.Controller.extend({ return emojiUnescape(escapeExpression(topic.get('title'))); }, + @computed + allowUpload() { + return authorizesOneOrMoreExtensions(); + }, + actions: { cancelUpload() { this.set('model.uploadCancelled', true); @@ -512,7 +520,7 @@ export default Ember.Controller.extend({ if (result.responseJson.action === "create_post" || this.get('replyAsNewTopicDraft') || this.get('replyAsNewPrivateMessageDraft')) { this.destroyDraft(); } - if (this.get('model.action') === 'edit') { + if (this.get('model.editingPost')) { this.appEvents.trigger('post-stream:refresh', { id: parseInt(result.responseJson.id) }); if (result.responseJson.post.post_number === 1) { this.appEvents.trigger('header:update-topic', composer.get('topic')); @@ -670,8 +678,12 @@ export default Ember.Controller.extend({ composerModel.set('composeState', Composer.OPEN); composerModel.set('isWarning', false); + if (opts.usernames) { + this.set('model.targetUsernames', opts.usernames); + } + if (opts.topicTitle && opts.topicTitle.length <= this.siteSettings.max_topic_title_length) { - this.set('model.title', opts.topicTitle); + this.set('model.title', escapeExpression(opts.topicTitle)); } if (opts.topicCategoryId) { @@ -719,25 +731,26 @@ export default Ember.Controller.extend({ destroyDraft() { const key = this.get('model.draftKey'); if (key) { + if (key === 'new_topic') { + this.send('clearTopicDraft'); + } Draft.clear(key, this.get('model.draftSequence')); } }, cancelComposer() { - const self = this; - - return new Ember.RSVP.Promise(function (resolve) { - if (self.get('model.hasMetaData') || self.get('model.replyDirty')) { + return new Ember.RSVP.Promise((resolve) => { + if (this.get('model.hasMetaData') || this.get('model.replyDirty')) { bootbox.dialog(I18n.t("post.abandon.confirm"), [ { label: I18n.t("post.abandon.no_value") }, { label: I18n.t("post.abandon.yes_value"), 'class': 'btn-danger', - callback(result) { + callback: (result) => { if (result) { - self.destroyDraft(); - self.get('model').clearState(); - self.close(); + this.destroyDraft(); + this.get('model').clearState(); + this.close(); resolve(); } } @@ -745,9 +758,9 @@ export default Ember.Controller.extend({ ]); } else { // it is possible there is some sort of crazy draft with no body ... just give up on it - self.destroyDraft(); - self.get('model').clearState(); - self.close(); + this.destroyDraft(); + this.get('model').clearState(); + this.close(); resolve(); } }); @@ -773,11 +786,19 @@ export default Ember.Controller.extend({ @computed('model.categoryId', 'lastValidatedAt') categoryValidation(categoryId, lastValidatedAt) { - if( !this.siteSettings.allow_uncategorized_topics && !categoryId) { + if(!this.siteSettings.allow_uncategorized_topics && !categoryId) { return InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing'), lastShownAt: lastValidatedAt }); } }, + @computed('model.category', 'model.tags', 'lastValidatedAt') + tagValidation(category, tags, lastValidatedAt) { + const tagsArray = tags || []; + if (this.site.get('can_tag_topics') && category && category.get('minimum_required_tags') > tagsArray.length) { + return InputValidation.create({ failed: true, reason: I18n.t('composer.error.tags_missing', {count: category.get('minimum_required_tags')}), lastShownAt: lastValidatedAt }); + } + }, + collapse() { this._saveDraft(); this.set('model.composeState', Composer.DRAFT); diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 9b3528f32b4..1ac854e9fa1 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -157,21 +157,18 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U }, createAccount() { - const self = this, - attrs = this.getProperties('accountName', 'accountEmail', 'accountPassword', 'accountUsername', 'accountPasswordConfirm', 'accountChallenge'), - userFields = this.get('userFields'); + const attrs = this.getProperties('accountName', 'accountEmail', 'accountPassword', 'accountUsername', 'accountPasswordConfirm', 'accountChallenge'); + const userFields = this.get('userFields'); // Add the userfields to the data if (!Ember.isEmpty(userFields)) { attrs.userFields = {}; - userFields.forEach(function(f) { - attrs.userFields[f.get('field.id')] = f.get('value'); - }); + userFields.forEach(f => attrs.userFields[f.get('field.id')] = f.get('value')); } this.set('formSubmitted', true); - return Discourse.User.createAccount(attrs).then(function(result) { - self.set('isDeveloper', false); + return Discourse.User.createAccount(attrs).then(result => { + this.set('isDeveloper', false); if (result.success) { // Trigger the browser's password manager using the hidden static login form: const $hidden_login_form = $('#hidden-login-form'); @@ -180,24 +177,21 @@ export default Ember.Controller.extend(ModalFunctionality, PasswordValidation, U $hidden_login_form.find('input[name=redirect]').val(userPath('account-created')); $hidden_login_form.submit(); } else { - self.flash(result.message || I18n.t('create_account.failed'), 'error'); + this.flash(result.message || I18n.t('create_account.failed'), 'error'); if (result.is_developer) { - self.set('isDeveloper', true); + this.set('isDeveloper', true); } if (result.errors && result.errors.email && result.errors.email.length > 0 && result.values) { - self.get('rejectedEmails').pushObject(result.values.email); + this.get('rejectedEmails').pushObject(result.values.email); } if (result.errors && result.errors.password && result.errors.password.length > 0) { - self.get('rejectedPasswords').pushObject(attrs.accountPassword); + this.get('rejectedPasswords').pushObject(attrs.accountPassword); } - self.set('formSubmitted', false); + this.set('formSubmitted', false); } - if (result.active && !Discourse.SiteSettings.must_approve_users) { - return window.location.reload(); - } - }, function() { - self.set('formSubmitted', false); - return self.flash(I18n.t('create_account.failed'), 'error'); + }, () => { + this.set('formSubmitted', false); + return this.flash(I18n.t('create_account.failed'), 'error'); }); } } diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index e955208acc2..344b79baff6 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -42,7 +42,7 @@ const controllerOpts = { const tracker = this.topicTrackingState; // Move inserted into topics - this.get('content').loadBefore(tracker.get('newIncoming')); + this.get('content').loadBefore(tracker.get('newIncoming'), true); tracker.resetTracking(); return false; }, @@ -137,11 +137,12 @@ const controllerOpts = { footerEducation: function() { if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !this.currentUser) { return; } - const split = (this.get('model.filter') || '').split('/'); + const segments = (this.get('model.filter') || '').split('/'); - if (split[0] !== 'new' && split[0] !== 'unread') { return; } + const tab = segments[segments.length - 1]; + if (tab !== 'new' && tab !== 'unread') { return; } - return I18n.t("topics.none.educate." + split[0], { + return I18n.t("topics.none.educate." + tab, { userPrefsUrl: userPath(`${this.currentUser.get('username_lower')}/preferences`) }); }.property('allLoaded', 'model.topics.length') diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 index e1a122a37a7..c12ae43a591 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 @@ -15,12 +15,15 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed("model.closed") publicTimerTypes(closed) { - return [ + let types = [ { id: CLOSE_STATUS_TYPE, name: I18n.t(closed ? 'topic.temp_open.title' : 'topic.auto_close.title'), }, { id: OPEN_STATUS_TYPE, name: I18n.t(closed ? 'topic.auto_reopen.title' : 'topic.temp_close.title') }, { id: PUBLISH_TO_CATEGORY_STATUS_TYPE, name: I18n.t('topic.publish_to_category.title') }, - { id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') } ]; + if (this.currentUser.get("staff")) { + types.push({ id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') }); + } + return types; }, @computed() @@ -32,20 +35,12 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed("isPublic", 'publicTimerTypes', 'privateTimerTypes') selections(isPublic, publicTimerTypes, privateTimerTypes) { - if (isPublic === 'true') { - return publicTimerTypes; - } else { - return privateTimerTypes; - } + return "true" === isPublic ? publicTimerTypes : privateTimerTypes; }, @computed('isPublic', 'model.topic_timer', 'model.private_topic_timer') topicTimer(isPublic, publicTopicTimer, privateTopicTimer) { - if (isPublic === 'true') { - return publicTopicTimer; - } else { - return privateTopicTimer; - } + return "true" === isPublic ? publicTopicTimer : privateTopicTimer; }, _setTimer(time, statusType) { diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6 index e8d309c8c98..87b9daf0012 100644 --- a/app/assets/javascripts/discourse/controllers/flag.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag.js.es6 @@ -80,7 +80,7 @@ export default Ember.Controller.extend(ModalFunctionality, { if (selected.get('is_custom_flag')) { const len = this.get('message.length') || 0; - return len >= Discourse.SiteSettings.min_private_message_post_length && + return len >= Discourse.SiteSettings.min_personal_message_post_length && len <= MAX_MESSAGE_LENGTH; } return true; diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 index 4311368186c..dd9dacc1a4a 100644 --- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 +++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 @@ -20,48 +20,54 @@ export default Ember.Controller.extend(ModalFunctionality, { }, actions: { - submit() { - if (this.get('submitDisabled')) return false; - - this.set('disabled', true); - - ajax('/session/forgot_password', { - data: { login: this.get('accountEmailOrUsername').trim() }, - type: 'POST' - }).then(data => { - const escaped = escapeExpression(this.get('accountEmailOrUsername')); - const isEmail = this.get('accountEmailOrUsername').match(/@/); - let key = 'forgot_password.complete_' + (isEmail ? 'email' : 'username'); - let extraClass; - - if (data.user_found === true) { - key += '_found'; - this.set('accountEmailOrUsername', ''); - this.set('offerHelp', I18n.t(key, {email: escaped, username: escaped})); - } else { - if (data.user_found === false) { - key += '_not_found'; - extraClass = 'error'; - } - - this.flash(I18n.t(key, {email: escaped, username: escaped}), extraClass); - } - }).catch(e => { - this.flash(extractError(e), 'error'); - }).finally(() => { - setTimeout(() => this.set('disabled', false), 1000); - }); - - return false; - }, - ok() { this.send('closeModal'); }, help() { this.setProperties({ offerHelp: I18n.t('forgot_password.help'), helpSeen: true }); - } - } + }, + resetPassword() { + if (this.get('submitDisabled')) return false; + this.set('disabled', true); + + this.clearFlash(); + + ajax('/session/forgot_password', { + data: { login: this.get('accountEmailOrUsername').trim() }, + type: 'POST' + }).then(data => { + const accountEmailOrUsername = escapeExpression(this.get("accountEmailOrUsername")); + const isEmail = accountEmailOrUsername.match(/@/); + let key = `forgot_password.complete_${isEmail ? 'email' : 'username'}`; + let extraClass; + + if (data.user_found === true) { + key += '_found'; + this.set('accountEmailOrUsername', ''); + this.set('offerHelp', I18n.t(key, { + email: accountEmailOrUsername, + username: accountEmailOrUsername + })); + } else { + if (data.user_found === false) { + key += '_not_found'; + extraClass = 'error'; + } + + this.flash(I18n.t(key, { + email: accountEmailOrUsername, + username: accountEmailOrUsername + }), extraClass); + } + }).catch(e => { + this.flash(extractError(e), 'error'); + }).finally(() => { + this.set('disabled', false); + }); + + return false; + } + }, }); diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index 741e74a8978..5534be1f99c 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -146,6 +146,11 @@ export default Ember.Controller.extend({ } }, + @computed('q') + isPrivateMessage(q) { + return q && this.currentUser && (q.indexOf("in:private") > -1 || q.indexOf(`private_messages:${this.currentUser.get('username_lower')}`) > -1); + }, + @observes('loading') _showFooter() { this.set("application.showFooter", !this.get("loading")); @@ -167,14 +172,14 @@ export default Ember.Controller.extend({ return this.currentUser && this.currentUser.staff && hasResults; }, - @computed('expanded', 'model.grouped_search_result.can_create_topic') - canCreateTopic(expanded, userCanCreateTopic) { - return this.currentUser && userCanCreateTopic && !this.site.mobileView && !expanded; + @computed('model.grouped_search_result.can_create_topic') + canCreateTopic(userCanCreateTopic) { + return this.currentUser && userCanCreateTopic; }, @computed('expanded') searchAdvancedIcon(expanded) { - return iconHTML(expanded ? "caret-down" : "caret-right"); + return iconHTML(expanded ? "caret-down fa-fw" : "caret-right fa-fw"); }, @computed('page') @@ -231,6 +236,7 @@ export default Ember.Controller.extend({ } }else{ setTransient('lastSearch', { searchKey, model }, 5); + model.grouped_search_result = results.grouped_search_result; this.set("model", model); } }).finally(() => { diff --git a/app/assets/javascripts/discourse/controllers/grant-badge.js.es6 b/app/assets/javascripts/discourse/controllers/grant-badge.js.es6 new file mode 100644 index 00000000000..8a33d053ff2 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/grant-badge.js.es6 @@ -0,0 +1,63 @@ +import computed from "ember-addons/ember-computed-decorators"; +import { extractError } from 'discourse/lib/ajax-error'; +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import GrantBadgeController from "discourse/mixins/grant-badge-controller"; +import Badge from 'discourse/models/badge'; +import UserBadge from 'discourse/models/user-badge'; + +export default Ember.Controller.extend(ModalFunctionality, GrantBadgeController, { + topicController: Ember.inject.controller("topic"), + loading: true, + saving: false, + selectedBadgeId: null, + allBadges: [], + userBadges: [], + + @computed('topicController.selectedPosts') + post() { + return this.get('topicController.selectedPosts')[0]; + }, + + @computed('post') + badgeReason(post) { + const url = post.get('url'); + const protocolAndHost = window.location.protocol + '//' + window.location.host; + + return url.indexOf('/') === 0 ? protocolAndHost + url : url; + }, + + @computed("saving", "selectedBadgeGrantable") + buttonDisabled(saving, selectedBadgeGrantable) { + return saving || !selectedBadgeGrantable; + }, + + onShow() { + this.set('loading', true); + + Ember.RSVP.all([Badge.findAll(), UserBadge.findByUsername(this.get('post.username'))]) + .then(([allBadges, userBadges]) => { + this.setProperties({ + 'allBadges': allBadges, + 'userBadges': userBadges, + 'loading': false, + }); + }); + }, + + actions: { + grantBadge() { + this.set('saving', true); + + this.grantBadge(this.get('selectedBadgeId'), this.get('post.username'), this.get('badgeReason')) + .then(newBadge => { + this.set('selectedBadgeId', null); + this.flash(I18n.t( + 'badges.successfully_granted', { username: this.get('post.username'), badge: newBadge.get('badge.name') } + ), 'success'); + }, error => { + this.flash(extractError(error), 'error'); + }) + .finally(() => this.set('saving', false)); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 index 4047450a1cc..f0b6b757f74 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 @@ -24,7 +24,7 @@ export default Ember.Controller.extend({ group.findPosts(opts).then(newPosts => { posts.addObjects(newPosts); - if(newPosts.length === 0) { + if (newPosts.length === 0) { this.set('canLoadMore', false); } }).finally(() => { diff --git a/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6 new file mode 100644 index 00000000000..e538cb9c77f --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-activity-topics.js.es6 @@ -0,0 +1,7 @@ +export default Ember.Controller.extend({ + actions: { + loadMore() { + this.get('model').loadMore(); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity.js.es6 index de4bda4dcdd..2e85014b8e3 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity.js.es6 @@ -1,11 +1,4 @@ -import computed from 'ember-addons/ember-computed-decorators'; - export default Ember.Controller.extend({ application: Ember.inject.controller(), queryParams: ['category_id'], - - @computed('model.is_group_user') - showGroupMessages(isGroupUser) { - return isGroupUser || (this.currentUser && this.currentUser.admin); - } }); diff --git a/app/assets/javascripts/discourse/controllers/group-add-members.js.es6 b/app/assets/javascripts/discourse/controllers/group-add-members.js.es6 new file mode 100644 index 00000000000..ac65416c5ca --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-add-members.js.es6 @@ -0,0 +1,45 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import { extractError } from 'discourse/lib/ajax-error'; +import ModalFunctionality from 'discourse/mixins/modal-functionality'; + +export default Ember.Controller.extend(ModalFunctionality, { + loading: false, + setAsOwner: false, + + @computed('model.usernames', 'loading') + disableAddButton(usernames, loading) { + return loading || !usernames || !(usernames.length > 0); + }, + + actions: { + addMembers() { + this.set('loading', true); + + const model = this.get('model'); + const usernames = model.get('usernames'); + if (Em.isEmpty(usernames)) { return; } + let promise; + + if (this.get('setAsOwner')) { + promise = model.addOwners(usernames, true); + } else { + promise = model.addMembers(usernames, true); + } + + promise.then(() => { + this.transitionToRoute( + "group.members", + this.get('model.name'), + { queryParams: { filter: usernames } } + ); + + model.set("usernames", null); + this.send('closeModal'); + }) + .catch(error => { + this.flash(extractError(error), 'error'); + }) + .finally(() => this.set('loading', false)); + }, + }, +}); diff --git a/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6 b/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6 new file mode 100644 index 00000000000..5469cc1235e --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-bulk-add.js.es6 @@ -0,0 +1,45 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import { extractError } from 'discourse/lib/ajax-error'; +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import { ajax } from 'discourse/lib/ajax'; + +export default Ember.Controller.extend(ModalFunctionality, { + loading: false, + + @computed('input', 'loading', 'result') + disableAddButton(input, loading, result) { + return loading || Ember.isEmpty(input) || (input.length <= 0) || result; + }, + + actions: { + cancel() { + this.set('result', null); + }, + + add() { + this.setProperties({ + loading: true, + result: null, + }); + + const users = this.get('input').split("\n") + .uniq() + .reject(x => x.length === 0); + + ajax('/admin/groups/bulk', { + data: { users, group_id: this.get('model.id') }, + method: 'PUT' + }).then(result => { + this.set('result', result); + + if (result.users_not_added) { + this.set('result.invalidUsers', result.users_not_added.join(", ")); + } + }).catch(error => { + this.flash(extractError(error), 'error'); + }).finally(() => { + this.set('loading', false); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/group-edit.js.es6 b/app/assets/javascripts/discourse/controllers/group-edit.js.es6 deleted file mode 100644 index 32277430af5..00000000000 --- a/app/assets/javascripts/discourse/controllers/group-edit.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import computed from 'ember-addons/ember-computed-decorators'; - -export default Ember.Controller.extend({ - @computed('saving') - savingText(saving) { - if (saving !== undefined) { - return saving ? I18n.t('saving') : I18n.t('saved'); - } - }, - - actions: { - save() { - this.set('saving', true); - - this.get('model').save().catch(error => { - popupAjaxError(error); - }).finally(() => { - this.set('saving', false); - }); - } - } -}); diff --git a/app/assets/javascripts/discourse/controllers/group-index.js.es6 b/app/assets/javascripts/discourse/controllers/group-index.js.es6 index 269ecab7cdb..c58b0a71842 100644 --- a/app/assets/javascripts/discourse/controllers/group-index.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-index.js.es6 @@ -1,9 +1,10 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import Group from 'discourse/models/group'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import debounce from 'discourse/lib/debounce'; export default Ember.Controller.extend({ - queryParams: ['order', 'desc'], + queryParams: ['order', 'desc', 'filter'], order: '', desc: null, loading: false, @@ -11,15 +12,32 @@ export default Ember.Controller.extend({ offset: null, isOwner: Ember.computed.alias('model.is_group_owner'), showActions: false, + filter: null, + filterInput: null, + application: Ember.inject.controller(), - @observes('order', 'desc') + @observes("filterInput") + _setFilter: debounce(function() { + this.set("filter", this.get("filterInput")); + }, 500), + + @observes('order', 'desc', 'filter') refreshMembers() { this.set('loading', true); + const model = this.get('model'); - this.get('model') && - this.get('model') - .findMembers({ order: this.get('order'), desc: this.get('desc') }) - .finally(() => this.set('loading', false)); + if (model) { + model.findMembers(this.get('memberParams')) + .finally(() => { + this.set('application.showFooter', model.members.length >= model.user_count); + this.set('loading', false); + }); + } + }, + + @computed('order', 'desc', 'filter') + memberParams(order, desc, filter) { + return { order, desc, filter }; }, @computed('model.members') @@ -27,13 +45,35 @@ export default Ember.Controller.extend({ return members && members.length > 0; }, + @computed('model') + canManageGroup(model) { + return this.currentUser && this.currentUser.canManageGroup(model); + }, + + @computed + filterPlaceholder() { + if (this.currentUser && this.currentUser.admin) { + return "groups.members.filter_placeholder_admin"; + } else { + return "groups.members.filter_placeholder"; + } + }, + actions: { toggleActions() { this.toggleProperty("showActions"); }, removeMember(user) { - this.get('model').removeMember(user); + this.get('model').removeMember(user, this.get('memberParams')); + }, + + makeOwner(username) { + this.get('model').addOwners(username); + }, + + removeOwner(user) { + this.get('model').removeOwner(user); }, addMembers() { @@ -45,7 +85,10 @@ export default Ember.Controller.extend({ loadMore() { if (this.get("loading")) { return; } - if (this.get("model.members.length") >= this.get("model.user_count")) { return; } + if (this.get("model.members.length") >= this.get("model.user_count")) { + this.set("application.showFooter", true); + return; + } this.set("loading", true); diff --git a/app/assets/javascripts/discourse/controllers/group-logs.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6 similarity index 90% rename from app/assets/javascripts/discourse/controllers/group-logs.js.es6 rename to app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6 index 0019127c3a6..bd6bb30472c 100644 --- a/app/assets/javascripts/discourse/controllers/group-logs.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6 @@ -4,6 +4,7 @@ export default Ember.Controller.extend({ group: Ember.inject.controller(), loading: false, offset: 0, + application: Ember.inject.controller(), init() { this._super(); @@ -27,6 +28,11 @@ export default Ember.Controller.extend({ }); }, + @observes("model.all_loaded") + _showFooter() { + this.set("application.showFooter", this.get("model.all_loaded")); + }, + reset() { this.setProperties({ offset: 0, diff --git a/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 new file mode 100644 index 00000000000..17df28ceb03 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Controller.extend({ + saving: null, +}); diff --git a/app/assets/javascripts/discourse/controllers/group-manage.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage.js.es6 new file mode 100644 index 00000000000..9d100f63e68 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-manage.js.es6 @@ -0,0 +1,25 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Controller.extend({ + application: Ember.inject.controller(), + + @computed("model.automatic") + tabs(automatic) { + const defaultTabs = [ + { route: 'group.manage.interaction', title: 'groups.manage.interaction.title' }, + { route: 'group.manage.logs', title: 'groups.manage.logs.title' }, + ]; + + if (!automatic) { + defaultTabs.splice(0, 0, + { route: 'group.manage.profile', title: 'groups.manage.profile.title' } + ); + + defaultTabs.splice(1, 0, + { route: 'group.manage.membership', title: 'groups.manage.membership.title' } + ); + } + + return defaultTabs; + }, +}); diff --git a/app/assets/javascripts/discourse/controllers/group-messages.js.es6 b/app/assets/javascripts/discourse/controllers/group-messages.js.es6 new file mode 100644 index 00000000000..63bf54eb78c --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-messages.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Controller.extend({ + application: Ember.inject.controller(), +}); diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index 98e4687f893..e9bf3bde7d1 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -1,4 +1,4 @@ -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; const Tab = Ember.Object.extend({ init() { @@ -13,17 +13,49 @@ export default Ember.Controller.extend({ application: Ember.inject.controller(), counts: null, showing: 'members', + destroying: null, - tabs: [ - Tab.create({ name: 'members', route: 'group.index', icon: 'users' }), - Tab.create({ name: 'activity' }), - Tab.create({ - name: 'edit', i18nKey: 'edit.title', icon: 'pencil', admin: true - }), - Tab.create({ - name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', admin: true - }) - ], + @computed('showMessages', 'model.user_count', 'canManageGroup') + tabs(showMessages, userCount, canManageGroup) { + const membersTab = Tab.create({ + name: 'members', + route: 'group.index', + icon: 'users', + i18nKey: "members.title" + }); + + membersTab.set('count', userCount); + + const defaultTabs = [ + membersTab, + Tab.create({ name: 'activity' }) + ]; + + if (showMessages) { + defaultTabs.push(Tab.create({ + name: 'messages', i18nKey: 'messages' + })); + } + + if (canManageGroup) { + defaultTabs.push( + Tab.create({ + name: 'manage', i18nKey: 'manage.title', icon: 'wrench' + }) + ); + } + + return defaultTabs; + }, + + @computed('model.is_group_user') + showMessages(isGroupUser) { + if (!this.siteSettings.enable_personal_messages) { + return false; + } + + return isGroupUser || (this.currentUser && this.currentUser.admin); + }, @computed('model.is_group_owner', 'model.automatic') canEditGroup(isGroupOwner, automatic) { @@ -50,14 +82,40 @@ export default Ember.Controller.extend({ return this.currentUser && messageable; }, - @observes('model.user_count') - _setMembersTabCount() { - this.get('tabs')[0].set('count', this.get('model.user_count')); + @computed('model', 'model.automatic') + canManageGroup(model, automatic) { + return this.currentUser && ( + this.currentUser.canManageGroup(model) || + (this.currentUser.admin && automatic) + ); }, actions: { messageGroup() { this.send('createNewMessageViaParams', this.get('model.name')); - } + }, + + destroy() { + const group = this.get('model'); + this.set('destroying', true); + + bootbox.confirm( + I18n.t("admin.groups.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + confirmed => { + if (confirmed) { + group.destroy().then(() => { + this.transitionToRoute('groups.index'); + }).catch(error => { + Ember.Logger.error(error); + bootbox.alert(I18n.t("admin.groups.delete_failed")); + }).finally(() => this.set('destroying', false)); + } else { + this.set('destroying', false); + } + } + ); + }, } }); diff --git a/app/assets/javascripts/discourse/controllers/groups-index.js.es6 b/app/assets/javascripts/discourse/controllers/groups-index.js.es6 new file mode 100644 index 00000000000..e8f4098ae8f --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/groups-index.js.es6 @@ -0,0 +1,44 @@ +import debounce from 'discourse/lib/debounce'; +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Controller.extend({ + application: Ember.inject.controller(), + queryParams: ["order", "asc", "filter", "type"], + order: null, + asc: null, + filter: "", + type: null, + + @computed("model.extras.type_filters") + types(typeFilters) { + const types = []; + + if (typeFilters) { + typeFilters.forEach(type => { + types.push({ id: type, name: I18n.t(`groups.index.${type}_groups`) }); + }); + } + + return types; + }, + + @observes("filterInput") + _setFilter: debounce(function() { + this.set("filter", this.get("filterInput")); + }, 500), + + @observes("model.canLoadMore") + _showFooter() { + this.set("application.showFooter", !this.get("model.canLoadMore")); + }, + + actions: { + loadMore() { + this.get('model').loadMore(); + }, + + new() { + this.transitionToRoute("groups.new"); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/groups-new.js.es6 b/app/assets/javascripts/discourse/controllers/groups-new.js.es6 new file mode 100644 index 00000000000..17e81c5da87 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/groups-new.js.es6 @@ -0,0 +1,17 @@ +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Controller.extend({ + saving: null, + + actions: { + save() { + this.set('saving', true); + const group = this.get('model'); + + group.create().then(() => { + this.transitionToRoute("group.members", group.name); + }).catch(popupAjaxError) + .finally(() => this.set('saving', false)); + }, + } +}); diff --git a/app/assets/javascripts/discourse/controllers/groups.js.es6 b/app/assets/javascripts/discourse/controllers/groups.js.es6 deleted file mode 100644 index d8ce08f3227..00000000000 --- a/app/assets/javascripts/discourse/controllers/groups.js.es6 +++ /dev/null @@ -1,16 +0,0 @@ -import { observes } from 'ember-addons/ember-computed-decorators'; - -export default Ember.Controller.extend({ - application: Ember.inject.controller(), - - @observes("groups.canLoadMore") - _showFooter() { - this.set("application.showFooter", !this.get("groups.canLoadMore")); - }, - - actions: { - loadMore() { - this.get('groups').loadMore(); - } - } -}); diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 71509d9e137..178bb02234b 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -158,6 +158,11 @@ export default Ember.Controller.extend(ModalFunctionality, { return Group.findAll({ term: term, ignore_automatic: true }); }, + @computed('isPrivateTopic', 'isMessage') + includeMentionableGroups(isPrivateTopic, isMessage) { + return !isPrivateTopic && !isMessage; + }, + @computed('isMessage', 'emailOrUsername', 'invitingExistingUserToTopic') successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) { if (this.get('hasGroups')) { diff --git a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 index 94ccc5595a4..06fae57eb80 100644 --- a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 +++ b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 @@ -6,10 +6,8 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { jump() { - let where = parseInt(this.get('postNumber')); - if (where < 1) { where = 1; } - const max = this.get('topic.postStream.filteredPostsCount'); - if (where > max) { where = max; } + const max = this.get("topic.postStream.filteredPostsCount"); + const where = Math.min(max, Math.max(1, parseInt(this.get("postNumber")))); this.jumpToIndex(where); this.send('closeModal'); diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index c524939435b..710a2a084ab 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -4,6 +4,9 @@ import showModal from 'discourse/lib/show-modal'; import { setting } from 'discourse/lib/computed'; import { findAll } from 'discourse/models/login-method'; import { escape } from 'pretty-text/sanitizer'; +import { escapeExpression } from 'discourse/lib/utilities'; +import { extractError } from 'discourse/lib/ajax-error'; +import computed from 'ember-addons/ember-computed-decorators'; // This is happening outside of the app via popup const AuthErrors = [ @@ -11,7 +14,7 @@ const AuthErrors = [ 'awaiting_approval', 'awaiting_activation', 'admin_not_allowed_from_ip_address', - 'not_allowed_from_ip_address' + 'not_allowed_from_ip_address', ]; export default Ember.Controller.extend(ModalFunctionality, { @@ -23,14 +26,23 @@ export default Ember.Controller.extend(ModalFunctionality, { authenticate: null, loggingIn: false, loggedIn: false, + processingEmailLink: false, + showLoginButtons: true, canLoginLocal: setting('enable_local_logins'), + canLoginLocalWithEmail: setting('enable_local_logins_via_email'), loginRequired: Em.computed.alias('application.loginRequired'), resetForm: function() { - this.set('authenticate', null); - this.set('loggingIn', false); - this.set('loggedIn', false); + this.setProperties({ + 'authenticate': null, + 'loggingIn': false, + 'loggedIn': false, + 'secondFactorRequired': false, + 'showLoginButtons': true, + }); + $("#credentials").show(); + $("#second-factor").hide(); }, // Determines whether at least one login button is enabled @@ -38,9 +50,10 @@ export default Ember.Controller.extend(ModalFunctionality, { return findAll(this.siteSettings).length > 0; }.property(), - loginButtonText: function() { - return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title'); - }.property('loggingIn'), + @computed('loggingIn') + loginButtonLabel(loggingIn) { + return loggingIn ? 'login.logging_in' : 'login.title'; + }, loginDisabled: Em.computed.or('loggingIn', 'loggedIn'), @@ -54,6 +67,11 @@ export default Ember.Controller.extend(ModalFunctionality, { return this.get('loggingIn') || this.get('authenticate'); }.property('loggingIn', 'authenticate'), + @computed('canLoginLocalWithEmail', 'loginName', 'processingEmailLink') + showLoginWithEmailLink(canLoginLocalWithEmail, loginName, processingEmailLink) { + return canLoginLocalWithEmail && !Ember.isEmpty(loginName) && !processingEmailLink; + }, + actions: { login() { const self = this; @@ -67,13 +85,30 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('loggingIn', true); ajax("/session", { - data: { login: this.get('loginName'), password: this.get('loginPassword') }, - type: 'POST' + type: 'POST', + data: { + login: this.get('loginName'), + password: this.get('loginPassword'), + second_factor_token: this.get('loginSecondFactor') + }, }).then(function (result) { // Successful login if (result && result.error) { self.set('loggingIn', false); - if (result.reason === 'not_activated') { + + if (result.reason === 'invalid_second_factor' && !self.get('secondFactorRequired')) { + $('#modal-alert').hide(); + self.setProperties({ + 'secondFactorRequired': true, + 'showLoginButtons': false, + }); + + $("#credentials").hide(); + $("#second-factor").show(); + $("#second-factor input").focus(); + + return; + } else if (result.reason === 'not_activated') { self.send('showNotActivated', { username: self.get('loginName'), sentTo: escape(result.sent_to_email), @@ -182,6 +217,37 @@ export default Ember.Controller.extend(ModalFunctionality, { const forgotPasswordController = this.get('forgotPassword'); if (forgotPasswordController) { forgotPasswordController.set("accountEmailOrUsername", this.get("loginName")); } this.send("showForgotPassword"); + }, + + emailLogin() { + if (this.get('processingEmailLink')) { + return; + } + + if (Ember.isEmpty(this.get('loginName'))){ + this.flash(I18n.t('login.blank_username'), 'error'); + return; + } + + this.set('processingEmailLink', true); + + ajax('/u/email-login', { + data: { login: this.get('loginName').trim() }, + type: 'POST' + }).then(data => { + const loginName = escapeExpression(this.get('loginName')); + const isEmail = loginName.match(/@/); + let key = `email_login.complete_${isEmail ? 'email' : 'username'}`; + if (data.user_found === false) { + this.flash(I18n.t(`${key}_not_found`, { email: loginName, username: loginName }), 'error'); + } else { + this.flash(I18n.t(`${key}_found`, { email: loginName, username: loginName })); + } + }).catch(e => { + this.flash(extractError(e), 'error'); + }).finally(() => { + this.set('processingEmailLink', false); + }); } }, @@ -194,16 +260,28 @@ export default Ember.Controller.extend(ModalFunctionality, { }).property('authenticate'), authenticationComplete(options) { - const self = this; - function loginError(errorMsg, className) { + function loginError(errorMsg, className, callback) { showModal('login'); - Ember.run.next(function() { + + Ember.run.next(() => { + if (callback) callback(); self.flash(errorMsg, className || 'success'); self.set('authenticate', null); }); } + if (options.omniauth_disallow_totp) { + return loginError(I18n.t('login.omniauth_disallow_totp'), 'error', () => { + this.setProperties({ + 'loginName': options.email, + 'showLoginButtons': false, + }); + + $('#login-account-password').focus(); + }); + } + for (let i=0; i { if (result.success) { @@ -45,10 +47,22 @@ export default Ember.Controller.extend(PasswordValidation, { DiscourseURL.redirectTo(result.redirect_to || '/'); } } else { - if (result.errors && result.errors.password && result.errors.password.length > 0) { + if (result.errors && result.errors.user_second_factor) { + this.setProperties({ + secondFactorRequired: true, + password: null, + errorMessage: result.message + }); + } else if (this.get('secondFactorRequired')) { + this.setProperties({ + secondFactorRequired: false, + errorMessage: null + }); + } else if (result.errors && result.errors.password && result.errors.password.length > 0) { this.get('rejectedPasswords').pushObject(this.get('accountPassword')); this.get('rejectedPasswordsMessages').set(this.get('accountPassword'), result.errors.password[0]); } + if (result.message) { this.set('errorMessage', result.message); } diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 9dc6083f15c..40befe7218c 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -4,15 +4,17 @@ import { default as computed } from "ember-addons/ember-computed-decorators"; import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; import { setting } from 'discourse/lib/computed'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +import showModal from 'discourse/lib/show-modal'; export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, { - saveAttrNames: ['name'], + saveAttrNames: ['name', 'title'], canEditName: setting('enable_names'), canSaveUser: true, newNameInput: null, + newTitleInput: null, passwordProgress: null, @@ -30,9 +32,9 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, return I18n.t(this.siteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions'); }, - @computed("model.has_title_badges") - canSelectTitle(hasTitleBadges) { - return this.siteSettings.enable_badges && hasTitleBadges; + @computed('model.availableTitles') + canSelectTitle(availableTitles) { + return availableTitles.length > 0; }, @computed() @@ -47,6 +49,7 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, const model = this.get('model'); model.set('name', this.get('newNameInput')); + model.set('title', this.get('newTitleInput')); return model.save(this.get('saveAttrNames')).then(() => { this.set('saved', true); @@ -98,6 +101,10 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, } ]; bootbox.dialog(message, buttons, {"classes": "delete-account"}); + }, + + showTwoFactorModal() { + showModal('second-factor-intro'); } } }); diff --git a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 deleted file mode 100644 index c39fb863956..00000000000 --- a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 +++ /dev/null @@ -1,9 +0,0 @@ -import BadgeSelectController from "discourse/mixins/badge-select-controller"; - -export default Ember.Controller.extend(BadgeSelectController, { - - filteredList: function() { - return this.get('model').filterBy('badge.allow_title', true); - }.property('model') - -}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 deleted file mode 100644 index 9f820f9647f..00000000000 --- a/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import { ajax } from 'discourse/lib/ajax'; -import BadgeSelectController from "discourse/mixins/badge-select-controller"; - -export default Ember.Controller.extend(BadgeSelectController, { - filteredList: function() { - return this.get('model').filter(function(b) { - return !Ember.isEmpty(b.get('badge.image')); - }); - }.property('model'), - - actions: { - save: function() { - this.setProperties({ saved: false, saving: true }); - - ajax(this.get('user.path') + "/preferences/card-badge", { - type: "PUT", - data: { user_badge_id: this.get('selectedUserBadgeId') } - }).then(() => { - this.setProperties({ - saved: true, - saving: false, - "user.card_image_badge": this.get('selectedUserBadge.badge.image') - }); - }).catch(() => { - this.set('saving', false); - bootbox.alert(I18n.t('generic_error')); - }); - } - } -}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 index f394acd3e1e..6e27ea04e07 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/categories.js.es6 @@ -1,6 +1,6 @@ import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; import { popupAjaxError } from 'discourse/lib/ajax-error'; -import { default as computed } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend(PreferencesTabController, { saveAttrNames: [ @@ -12,7 +12,7 @@ export default Ember.Controller.extend(PreferencesTabController, { @computed("model.watchedCategories", "model.watchedFirstPostCategories", "model.trackedCategories", "model.mutedCategories") selectedCategories(watched, watchedFirst, tracked, muted) { - return [].concat(watched, watchedFirst, tracked, muted); + return [].concat(watched, watchedFirst, tracked, muted).filter(t => t); }, canSave: function() { diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 index fd688471bc9..9eb2960b836 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 @@ -57,16 +57,24 @@ export default Ember.Controller.extend(PreferencesTabController, { }, homeChanged() { - const siteHome = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + const siteHome = this.siteSettings.top_menu.split("|")[0].split(",")[0]; const userHome = USER_HOMES[this.get('model.user_option.homepage_id')]; + setDefaultHomepage(userHome || siteHome); }, @computed() userSelectableHome() { - return _.map(USER_HOMES, (name, num) => { - return {name: I18n.t('filters.' + name + '.title'), value: Number(num)}; + let homeValues = _.invert(USER_HOMES); + + let result = []; + this.siteSettings.top_menu.split('|').forEach(m => { + let id = homeValues[m]; + if (id) { + result.push({ name: I18n.t(`filters.${m}.title`), value: Number(id) }); + } }); + return result; }, actions: { diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 new file mode 100644 index 00000000000..bdaab628cd9 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 @@ -0,0 +1,109 @@ +import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { default as DiscourseURL, userPath } from 'discourse/lib/url'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { LOGIN_METHODS } from 'discourse/models/login-method'; + +export default Ember.Controller.extend({ + loading: false, + resetPasswordLoading: false, + resetPasswordProgress: '', + password: null, + secondFactorImage: null, + secondFactorKey: null, + showSecondFactorKey: false, + errorMessage: null, + newUsername: null, + + loaded: Ember.computed.and('secondFactorImage', 'secondFactorKey'), + + @computed('loading') + submitButtonText(loading) { + return loading ? 'loading' : 'continue'; + }, + + @computed('loading') + enableButtonText(loading) { + return loading ? 'loading' : 'enable'; + }, + + @computed('loading') + disableButtonText(loading) { + return loading ? 'loading' : 'disable'; + }, + + @computed + displayOAuthWarning() { + return LOGIN_METHODS.some(name => { + return this.siteSettings[`enable_${name}_logins`]; + }); + }, + + toggleSecondFactor(enable) { + if (!this.get('secondFactorToken')) return; + this.set('loading', true); + + this.get('content').toggleSecondFactor(this.get('secondFactorToken'), enable) + .then(response => { + if (response.error) { + this.set('errorMessage', response.error); + this.set('loading', false); + return; + } + + this.set('errorMessage',null); + DiscourseURL.redirectTo(userPath(`${this.get('content').username.toLowerCase()}/preferences`)); + }) + .catch(error => { + this.set('loading', false); + popupAjaxError(error); + }); + }, + + actions: { + confirmPassword() { + if (!this.get('password')) return; + this.set('loading', true); + + this.get('content').loadSecondFactorCodes(this.get('password')) + .then(response => { + if(response.error) { + this.set('errorMessage', response.error); + return; + } + + this.setProperties({ + errorMessage: null, + secondFactorKey: response.key, + secondFactorImage: response.qr, + }); + }) + .catch(popupAjaxError) + .finally(() => this.set('loading', false)); + }, + + resetPassword() { + this.setProperties({ + resetPasswordLoading: true, + resetPasswordProgress: '' + }); + + return this.get('model').changePassword().then(() => { + this.set('resetPasswordProgress', I18n.t('user.change_password.success')); + }) + .catch(popupAjaxError) + .finally(() => this.set('resetPasswordLoading', false)); + }, + + showSecondFactorKey() { + this.set('showSecondFactorKey', true); + }, + + enableSecondFactor() { + this.toggleSecondFactor(true); + }, + + disableSecondFactor() { + this.toggleSecondFactor(false); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 index 6264d42b2de..39608df5b45 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/tags.js.es6 @@ -1,8 +1,8 @@ import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; import { popupAjaxError } from 'discourse/lib/ajax-error'; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend(PreferencesTabController, { - saveAttrNames: [ 'muted_tags', 'tracked_tags', @@ -10,6 +10,11 @@ export default Ember.Controller.extend(PreferencesTabController, { 'watching_first_post_tags' ], + @computed("model.watched_tags.[]", "model.watching_first_post_tags.[]", "model.tracked_tags.[]", "model.muted_tags.[]") + selectedTags(watched, watchedFirst, tracked, muted) { + return [].concat(watched, watchedFirst, tracked, muted).filter(t => t); + }, + actions: { save() { this.set('saved', false); diff --git a/app/assets/javascripts/discourse/controllers/tags-index.js.es6 b/app/assets/javascripts/discourse/controllers/tags-index.js.es6 index 07a9da7cfed..acf1c58d466 100644 --- a/app/assets/javascripts/discourse/controllers/tags-index.js.es6 +++ b/app/assets/javascripts/discourse/controllers/tags-index.js.es6 @@ -1,15 +1,38 @@ +import computed from 'ember-addons/ember-computed-decorators'; + export default Ember.Controller.extend({ - sortProperties: ['count:desc', 'id'], + sortProperties: ['totalCount:desc', 'id'], + sortedByCount: true, + sortedByName: false, canAdminTags: Ember.computed.alias("currentUser.staff"), + groupedByCategory: Ember.computed.notEmpty('model.extras.categories'), + groupedByTagGroup: Ember.computed.notEmpty('model.extras.tag_groups'), + + @computed('groupedByCategory', 'groupedByTagGroup') + otherTagsTitleKey(groupedByCategory, groupedByTagGroup) { + if (!groupedByCategory && !groupedByTagGroup) { + return 'tagging.all_tags'; + } else { + return 'tagging.other_tags'; + } + }, actions: { sortByCount() { - this.set('sortProperties', ['count:desc', 'id']); + this.setProperties({ + sortProperties: ['totalCount:desc', 'id'], + sortedByCount: true, + sortedByName: false + }); }, sortById() { - this.set('sortProperties', ['id']); + this.setProperties({ + sortProperties: ['id'], + sortedByCount: false, + sortedByName: true + }); } } }); diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 index d1a292c2e88..643d1095b22 100644 --- a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 @@ -61,6 +61,10 @@ export default Ember.Controller.extend(BulkTopicSelection, { categories: Ember.computed.alias('site.categoriesList'), + createTopicLabel: function() { + return this.get('list.draft') ? 'topic.open_draft' : 'topic.create'; + }.property('list', 'list.draft'), + @computed('canCreateTopic', 'category', 'canCreateTopicOnCategory') createTopicDisabled(canCreateTopic, category, canCreateTopicOnCategory) { return !canCreateTopic || (category && !canCreateTopicOnCategory); @@ -119,8 +123,8 @@ export default Ember.Controller.extend(BulkTopicSelection, { deleteTag() { const self = this; - const topicsLength = this.get('list.topic_list.topics.length'); - const confirmText = topicsLength === 0 ? I18n.t("tagging.delete_confirm_no_topics") : I18n.t("tagging.delete_confirm", {count: topicsLength}); + const numTopics = this.get('list.topic_list.tags.firstObject.topic_count') || 0; + const confirmText = numTopics === 0 ? I18n.t("tagging.delete_confirm_no_topics") : I18n.t("tagging.delete_confirm", {count: numTopics}); bootbox.confirm(confirmText, function(result) { if (!result) { return; } diff --git a/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 deleted file mode 100644 index a2611ebdcbd..00000000000 --- a/app/assets/javascripts/discourse/controllers/topic-unsubscribe.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -export default Ember.Controller.extend({ - - stopNotificiationsText: function() { - return I18n.t("topic.unsubscribe.stop_notifications", { title: this.get("model.fancyTitle") }); - }.property("model.fancyTitle"), - -}); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 80209d7b731..9a464c463cf 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -14,6 +14,20 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import { userPath } from 'discourse/lib/url'; +let customPostMessageCallbacks = {}; + +export function resetCustomPostMessageCallbacks() { + customPostMessageCallbacks = {}; +} + +export function registerCustomPostMessageCallback(type, callback) { + if (customPostMessageCallbacks[type]) { + throw `Error ${type} is an already registered post message!`; + } + + customPostMessageCallbacks[type] = callback; +} + export default Ember.Controller.extend(BufferedContent, { composer: Ember.inject.controller(), application: Ember.inject.controller(), @@ -46,6 +60,12 @@ export default Ember.Controller.extend(BufferedContent, { } }, + @computed('model.postStream.loaded', 'model.category_id') + showSharedDraftControls(loaded, categoryId) { + let draftCat = this.site.shared_drafts_category_id; + return loaded && draftCat && categoryId && draftCat === categoryId; + }, + @computed('site.mobileView', 'model.posts_count') showSelectedPostsAtBottom(mobileView, postsCount) { return mobileView && postsCount > 3; @@ -104,7 +124,7 @@ export default Ember.Controller.extend(BufferedContent, { @computed('model.isPrivateMessage') canEditTags(isPrivateMessage) { - return !isPrivateMessage && this.site.get('can_tag_topics'); + return this.site.get('can_tag_topics') && (!isPrivateMessage || this.site.get('can_tag_pms')); }, actions: { @@ -125,7 +145,6 @@ export default Ember.Controller.extend(BufferedContent, { return this.get('model.postStream').loadPost(postId).then(post => { const composer = this.get('composer'); const viewOpen = composer.get('model.viewOpen'); - const quotedText = Quote.build(post, buffer); // If we can't create a post, delegate to reply as new topic @@ -199,7 +218,7 @@ export default Ember.Controller.extend(BufferedContent, { }); }, - // Called the the topmost visible post on the page changes. + // Called when the topmost visible post on the page changes. topVisibleChanged(event) { const { post, refresh } = event; if (!post) { return; } @@ -265,6 +284,23 @@ export default Ember.Controller.extend(BufferedContent, { } }, + editFirstPost() { + const postStream = this.get('model.postStream'); + let firstPost = postStream.get('posts.firstObject'); + + if (firstPost.get('post_number') !== 1) { + const postId = postStream.findPostIdForPostNumber(1); + // try loading from identity map first + firstPost = postStream.findLoadedPost(postId); + if (firstPost === undefined) { + return this.get('model.postStream').loadPost(postId).then(post => { + this.send("editPost", post); + }); + } + } + this.send("editPost", firstPost); + }, + // Post related methods replyToPost(post) { const composerController = this.get('composer'); @@ -386,16 +422,29 @@ export default Ember.Controller.extend(BufferedContent, { } const composer = this.get("composer"); + let topic = this.get('model'); const composerModel = composer.get("model"); + let editingFirst = composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost')); + + let editingSharedDraft = false; + let draftsCategoryId = this.get('site.shared_drafts_category_id'); + if (draftsCategoryId && draftsCategoryId === topic.get('category.id')) { + editingSharedDraft = post.get('firstPost'); + } + const opts = { post, - action: Composer.EDIT, + action: editingSharedDraft ? Composer.EDIT_SHARED_DRAFT : Composer.EDIT, draftKey: post.get("topic.draft_key"), draftSequence: post.get("topic.draft_sequence") }; + if (editingSharedDraft) { + opts.destinationCategoryId = topic.get('destination_category_id'); + } + // Cancel and reopen the composer for the first post - if (composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost'))) { + if (editingFirst) { composer.cancelComposer().then(() => composer.open(opts)); } else { composer.open(opts); @@ -416,17 +465,17 @@ export default Ember.Controller.extend(BufferedContent, { }, jumpToIndex(index) { - this._jumpToPostId(this.get('model.postStream.stream')[index - 1]); + this._jumpToIndex(index); }, jumpToPostPrompt() { const postText = prompt(I18n.t('topic.progress.jump_prompt_long')); if (postText === null) { return; } - const postNumber = parseInt(postText, 10); - if (postNumber === 0) { return; } + const postIndex = parseInt(postText, 10); + if (postIndex === 0) { return; } - this._jumpToPostId(this.get('model.postStream').findPostIdForPostNumber(postNumber)); + this._jumpToIndex(postIndex); }, jumpToPost(postNumber) { @@ -454,6 +503,10 @@ export default Ember.Controller.extend(BufferedContent, { this._jumpToPostId(this.get('model.last_read_post_id')); }, + jumpToPostId(postId) { + this._jumpToPostId(postId); + }, + toggleMultiSelect() { this.toggleProperty('multiSelect'); this.appEvents.trigger('post-stream:refresh', { force: true }); @@ -518,6 +571,19 @@ export default Ember.Controller.extend(BufferedContent, { this.send('changeOwner'); }, + lockPost(post) { + return post.updatePostField('locked', true); + }, + + unlockPost(post) { + return post.updatePostField('locked', false); + }, + + grantBadge(post) { + this.set("selectedPostIds", [post.id]); + this.send('showGrantBadgeModal'); + }, + toggleParticipant(user) { this.get("model.postStream") .toggleParticipant(user.get("username")) @@ -702,6 +768,12 @@ export default Ember.Controller.extend(BufferedContent, { } }, + _jumpToIndex(index) { + const stream = this.get("model.postStream.stream"); + index = Math.max(1, Math.min(stream.length, index)); + this._jumpToPostId(stream[index - 1]); + }, + _jumpToPostId(postId) { if (!postId) { Ember.Logger.warn("jump-post code broken - requested an index outside the stream array"); @@ -767,9 +839,12 @@ export default Ember.Controller.extend(BufferedContent, { return selectedPostsCount > 0 && (selectedAllPosts || selectedPosts.every(p => p.can_delete)); }, - @computed('canMergeTopic', 'selectedAllPosts') - canSplitTopic(canMergeTopic, selectedAllPosts) { - return canMergeTopic && !selectedAllPosts; + @computed('canMergeTopic', 'selectedAllPosts', 'selectedPosts', 'selectedPosts.[]') + canSplitTopic(canMergeTopic, selectedAllPosts, selectedPosts) { + return canMergeTopic && + !selectedAllPosts && + selectedPosts.length > 0 && + selectedPosts.sort((a, b) => a.post_number - b.post_number)[0].post_type === 1; }, @computed('model.details.can_move_posts', 'selectedPostsCount') @@ -874,7 +949,12 @@ export default Ember.Controller.extend(BufferedContent, { break; } default: { - Em.Logger.warn("unknown topic bus message type", data); + let callback = customPostMessageCallbacks[data.type]; + if (callback) { + callback(this, data); + } else { + Em.Logger.warn("unknown topic bus message type", data); + } } } diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6 new file mode 100644 index 00000000000..9d2cd890ed4 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/user-private-messages-tags.js.es6 @@ -0,0 +1,24 @@ +export default Ember.Controller.extend({ + sortProperties: ['count:desc', 'id'], + tagsForUser: null, + sortedByCount: true, + sortedByName: false, + + actions: { + sortByCount() { + this.setProperties({ + sortProperties: ['count:desc', 'id'], + sortedByCount: true, + sortedByName: false + }); + }, + + sortById() { + this.setProperties({ + sortProperties: ['id'], + sortedByCount: false, + sortedByName: true + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 index bb79db55d37..fa1aaca9421 100644 --- a/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-private-messages.js.es6 @@ -12,6 +12,9 @@ export default Ember.Controller.extend({ currentPath: Em.computed.alias('application.currentPath'), selected: Em.computed.alias('userTopicsList.selected'), bulkSelectEnabled: Em.computed.alias('userTopicsList.bulkSelectEnabled'), + showToggleBulkSelect: true, + pmTaggingEnabled: Ember.computed.alias('site.can_tag_pms'), + tagId: null, showNewPM: function(){ return this.get('user.viewingSelf') && @@ -33,7 +36,6 @@ export default Ember.Controller.extend({ return hasSelection && pmView !== "archive" && !archive; }, - bulkOperation(operation) { const selected = this.get('selected'); var params = {type: operation}; diff --git a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 index 8e150fd2890..0d119f6d331 100644 --- a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 @@ -1,18 +1,60 @@ +import computed from 'ember-addons/ember-computed-decorators'; + // Lists of topics on a user's page. export default Ember.Controller.extend({ application: Ember.inject.controller(), hideCategory: false, showPosters: false, + newIncoming: [], + incomingCount: 0, + channel: null, + tagsForUser: null, _showFooter: function() { this.set("application.showFooter", !this.get("model.canLoadMore")); }.observes("model.canLoadMore"), + @computed('incomingCount') + hasIncoming(incomingCount) { + return incomingCount > 0; + }, + + subscribe(channel) { + this.set('channel', channel); + + this.messageBus.subscribe(channel, data => { + if (this.get('newIncoming').indexOf(data.topic_id) === -1) { + this.get('newIncoming').push(data.topic_id); + this.incrementProperty('incomingCount'); + } + }); + }, + + unsubscribe() { + const channel = this.get('channel'); + if (channel) this.messageBus.unsubscribe(channel); + this._resetTracking(); + this.set('channel', null); + }, + + _resetTracking() { + this.setProperties({ + "newIncoming": [], + "incomingCount": 0 + }); + }, + actions: { loadMore: function() { this.get('model').loadMore(); - } + }, + + showInserted() { + this.get('model').loadBefore(this.get('newIncoming')); + this._resetTracking(); + return false; + }, }, }); diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 37e88e21acc..afa32314131 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -1,16 +1,24 @@ import CanCheckEmails from 'discourse/mixins/can-check-emails'; import computed from 'ember-addons/ember-computed-decorators'; import User from 'discourse/models/user'; +import optionalService from 'discourse/lib/optional-service'; export default Ember.Controller.extend(CanCheckEmails, { indexStream: false, application: Ember.inject.controller(), userNotifications: Ember.inject.controller('user-notifications'), currentPath: Ember.computed.alias('application.currentPath'), + adminTools: optionalService(), - @computed("content.username") + @computed('model.username') viewingSelf(username) { - return username === User.currentProp('username'); + let currentUser = this.currentUser; + return currentUser && username === currentUser.get('username'); + }, + + @computed('viewingSelf') + canExpandProfile(viewingSelf) { + return viewingSelf; }, @computed('model.profileBackground') @@ -31,9 +39,9 @@ export default Ember.Controller.extend(CanCheckEmails, { showStaffCounters: Ember.computed.or('hasGivenFlags', 'hasFlaggedPosts', 'hasDeletedPosts', 'hasBeenSuspended', 'hasReceivedWarnings'), - @computed('model.isSuspended', 'currentUser.staff') - isNotSuspendedOrIsStaff(isSuspended, isStaff) { - return !isSuspended || isStaff; + @computed('model.suspended', 'currentUser.staff') + isNotSuspendedOrIsStaff(suspended, isStaff) { + return !suspended || isStaff; }, linkWebsite: Em.computed.not('model.isBasic'), @@ -50,7 +58,7 @@ export default Ember.Controller.extend(CanCheckEmails, { @computed('viewingSelf', 'currentUser.admin') showPrivateMessages(viewingSelf, isAdmin) { - return this.siteSettings.enable_private_messages && (viewingSelf || isAdmin); + return this.siteSettings.enable_personal_messages && (viewingSelf || isAdmin); }, @computed('viewingSelf', 'currentUser.staff') @@ -89,15 +97,23 @@ export default Ember.Controller.extend(CanCheckEmails, { }, actions: { + collapseProfile() { + this.set('forceExpand', false); + }, + expandProfile() { this.set('forceExpand', true); }, - adminDelete() { - // I really want this deferred, don't want to bring in all this code till used - const AdminUser = requirejs('admin/models/admin-user').default; - AdminUser.find(this.get('model.id')).then(user => user.destroy({deletePosts: true})); + showSuspensions() { + this.get('adminTools').showActionLogs(this, { + target_user: this.get('model.username'), + action_name: 'suspend_user' + }); }, + adminDelete() { + this.get('adminTools').deleteUser(this.get('model.id')); + } } }); diff --git a/app/assets/javascripts/discourse/controllers/users.js.es6 b/app/assets/javascripts/discourse/controllers/users.js.es6 index f5bb1963025..cb2cafbc1f3 100644 --- a/app/assets/javascripts/discourse/controllers/users.js.es6 +++ b/app/assets/javascripts/discourse/controllers/users.js.es6 @@ -2,11 +2,13 @@ import debounce from 'discourse/lib/debounce'; export default Ember.Controller.extend({ application: Ember.inject.controller(), - queryParams: ["period", "order", "asc", "name"], + queryParams: ["period", "order", "asc", "name", "group", "exclude_usernames"], period: "weekly", order: "likes_received", asc: null, name: "", + group: null, + exclude_usernames: null, showTimeRead: Ember.computed.equal("period", "all"), diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index 5686e07d6b4..d43c1a77915 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -8,6 +8,10 @@ registerUnbound('raw-date', dt => longDate(new Date(dt))); registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}))); registerUnbound('number', (orig, params) => { + if (params.ceil) { + orig = Math.ceil(orig); + } + orig = parseInt(orig, 10); if (isNaN(orig)) { orig = 0; } diff --git a/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6 index 2833f8cefb1..a284fb36714 100644 --- a/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/bound-avatar.js.es6 @@ -1,11 +1,12 @@ import { htmlHelper } from 'discourse-common/lib/helpers'; import { avatarImg } from 'discourse/lib/utilities'; +import { addExtraUserClasses } from 'discourse/helpers/user-avatar'; export default htmlHelper((user, size) => { if (Ember.isEmpty(user)) { return "
      "; } - const avatarTemplate = Em.get(user, 'avatar_template'); - return avatarImg({ size, avatarTemplate }); + const avatarTemplate = Ember.get(user, 'avatar_template'); + return avatarImg(addExtraUserClasses(user, { size, avatarTemplate })); }); diff --git a/app/assets/javascripts/discourse/helpers/category-link.js.es6 b/app/assets/javascripts/discourse/helpers/category-link.js.es6 index d02254e31c7..7735be3d69b 100644 --- a/app/assets/javascripts/discourse/helpers/category-link.js.es6 +++ b/app/assets/javascripts/discourse/helpers/category-link.js.es6 @@ -1,4 +1,5 @@ import { registerUnbound } from 'discourse-common/lib/helpers'; +import { isRTL } from "discourse/lib/text-direction"; import { iconHTML } from 'discourse-common/lib/icon-library'; var get = Em.get, @@ -38,6 +39,7 @@ export function categoryBadgeHTML(category, opts) { let color = get(category, 'color'); let html = ""; let parentCat = null; + let categoryDir = ""; if (!opts.hideParent) { parentCat = Discourse.Category.findById(get(category, 'parent_category_id')); @@ -66,10 +68,14 @@ export function categoryBadgeHTML(category, opts) { let categoryName = escapeExpression(get(category, 'name')); + if (Discourse.SiteSettings.support_mixed_text_direction) { + categoryDir = isRTL(categoryName) ? 'dir="rtl"' : 'dir="ltr"'; + } + if (restricted) { - html += iconHTML('lock') + " " + categoryName; + html += `${iconHTML('lock')}${categoryName}`; } else { - html += categoryName; + html += `${categoryName}`; } html += ""; diff --git a/app/assets/javascripts/discourse/helpers/dir-span.js.es6 b/app/assets/javascripts/discourse/helpers/dir-span.js.es6 new file mode 100644 index 00000000000..3f258025651 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/dir-span.js.es6 @@ -0,0 +1,15 @@ +import { registerUnbound } from "discourse-common/lib/helpers"; +import { isRTL } from 'discourse/lib/text-direction'; + +function setDir(text) { + let content = text ? text : ""; + if (content && Discourse.SiteSettings.support_mixed_text_direction) { + let textDir = isRTL(content) ? 'rtl' : 'ltr'; + return `${content}`; + } + return content; +} + +export default registerUnbound('dir-span', function(str) { + return new Handlebars.SafeString(setDir(str)); +}); diff --git a/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6 b/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6 index d5439c7323f..44b9d32325c 100644 --- a/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6 +++ b/app/assets/javascripts/discourse/helpers/icon-or-image.js.es6 @@ -1,7 +1,14 @@ import { htmlHelper } from 'discourse-common/lib/helpers'; import { iconHTML } from 'discourse-common/lib/icon-library'; -export default htmlHelper(function(str) { - if (Ember.isEmpty(str)) { return ""; } - return (str.indexOf('fa-') === 0) ? iconHTML(str.replace('fa-', '')) : ``; +export default htmlHelper(function({ icon, image }) { + if (!Ember.isEmpty(image)) { + return ``; + } + + if (Ember.isEmpty(icon) || icon.indexOf('fa-') !== 0) { + return ''; + } + + return iconHTML(icon.replace('fa-', '')); }); diff --git a/app/assets/javascripts/discourse/helpers/period-title.js.es6 b/app/assets/javascripts/discourse/helpers/period-title.js.es6 index 37af602c29d..d5d398791e7 100644 --- a/app/assets/javascripts/discourse/helpers/period-title.js.es6 +++ b/app/assets/javascripts/discourse/helpers/period-title.js.es6 @@ -12,25 +12,33 @@ export default htmlHelper((period, options) => { const title = I18n.t('filters.top.' + (TITLE_SUBS[period] || 'this_week')); if (options.hash.showDateRange) { var dateString = ""; + let finish; + + if (options.hash.fullDay) { + finish = moment().utc().subtract(1, 'days'); + } else { + finish = moment(); + } + switch(period) { case 'yearly': - dateString = moment().subtract(1, 'year').format(I18n.t('dates.long_with_year_no_time')) + " - " + moment().format(I18n.t('dates.long_with_year_no_time')); + dateString = finish.clone().subtract(1, 'year').format(I18n.t('dates.long_with_year_no_time')) + " - " + finish.format(I18n.t('dates.long_with_year_no_time')); break; case 'quarterly': - dateString = moment().subtract(3, 'month').format(I18n.t('dates.long_no_year_no_time')) + " - " + moment().format(I18n.t('dates.long_no_year_no_time')); + dateString = finish.clone().subtract(3, 'month').format(I18n.t('dates.long_no_year_no_time')) + " - " + finish.format(I18n.t('dates.long_no_year_no_time')); break; case 'weekly': - dateString = moment().subtract(1, 'week').format(I18n.t('dates.long_no_year_no_time')) + " - " + moment().format(I18n.t('dates.long_no_year_no_time')); + dateString = finish.clone().subtract(1, 'week').format(I18n.t('dates.long_no_year_no_time')) + " - " + finish.format(I18n.t('dates.long_no_year_no_time')); break; case 'monthly': - dateString = moment().subtract(1, 'month').format(I18n.t('dates.long_no_year_no_time')) + " - " + moment().format(I18n.t('dates.long_no_year_no_time')); + dateString = finish.clone().subtract(1, 'month').format(I18n.t('dates.long_no_year_no_time')) + " - " + finish.format(I18n.t('dates.long_no_year_no_time')); break; case 'daily': - dateString = moment().format(I18n.t('dates.full_no_year_no_time')); + dateString = finish.clone().format(I18n.t('dates.full_no_year_no_time')); break; } - return `${title} ${dateString}`; + return `${title}${dateString}`; } else { return title; } diff --git a/app/assets/javascripts/discourse/helpers/theme-setting-injector.es6 b/app/assets/javascripts/discourse/helpers/theme-setting-injector.es6 new file mode 100644 index 00000000000..b0c774c84ab --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/theme-setting-injector.es6 @@ -0,0 +1,36 @@ +// A small helper to inject theme settings into +// context objects of handlebars templates used +// in themes + +import { registerHelper } from 'discourse-common/lib/helpers'; + +function inject(context, key, value) { + if (typeof value === "string") { + value = value.replace(/\\u0022/g, '"'); + } + + if (!(context instanceof Ember.Object)) { + injectPlainObject(context, key, value); + return; + } + + if (!context.get("themeSettings")) { + context.set("themeSettings", {}); + } + context.set(`themeSettings.${key}`, value); +} + +function injectPlainObject(context, key, value) { + if (!context.themeSettings) { + _.assign(context, { themeSettings: {} }); + } + _.assign(context.themeSettings, { [key]: value }); +} + +registerHelper('theme-setting-injector', function(arr, hash) { + inject(hash.context, hash.key, hash.value); +}); + +Handlebars.registerHelper('theme-setting-injector', function(hash) { + inject(this, hash.hash.key, hash.hash.value); +}); diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 index 71e6e87249c..dc685eee6e5 100644 --- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 @@ -1,6 +1,31 @@ import { registerUnbound } from 'discourse-common/lib/helpers'; import { avatarImg, formatUsername } from 'discourse/lib/utilities'; +let _customAvatarHelpers; + +export function registerCustomAvatarHelper(fn) { + _customAvatarHelpers = _customAvatarHelpers || []; + _customAvatarHelpers.push(fn); +} + +export function addExtraUserClasses(u, args) { + let extraClasses = classesForUser(u).join(' '); + if (extraClasses && extraClasses.length) { + args.extraClasses = extraClasses; + } + return args; +} + +export function classesForUser(u) { + let result = []; + if (_customAvatarHelpers) { + for (let i=0; i<_customAvatarHelpers.length; i++) { + result = result.concat(_customAvatarHelpers[i](u)); + } + } + return result; +} + function renderAvatar(user, options) { options = options || {}; @@ -11,7 +36,7 @@ function renderAvatar(user, options) { if (!username || !avatarTemplate) { return ''; } - let formattedUsername = formatUsername(username); + let displayName = Ember.get(user, 'name') || formatUsername(username); let title = options.title; if (!title && !options.ignoreTitle) { @@ -24,7 +49,7 @@ function renderAvatar(user, options) { // if a description has been provided if (description && description.length > 0) { // preprend the username before the description - title = formattedUsername + " - " + description; + title = displayName + " - " + description; } } } @@ -32,7 +57,7 @@ function renderAvatar(user, options) { return avatarImg({ size: options.imageSize, extraClasses: Em.get(user, 'extras') || options.extraClasses, - title: title || formattedUsername, + title: title || displayName, avatarTemplate: avatarTemplate }); } else { diff --git a/app/assets/javascripts/discourse/initializers/banner.js.es6 b/app/assets/javascripts/discourse/initializers/banner.js.es6 index 9c9ff47d927..4ce88b2c480 100644 --- a/app/assets/javascripts/discourse/initializers/banner.js.es6 +++ b/app/assets/javascripts/discourse/initializers/banner.js.es6 @@ -5,7 +5,6 @@ export default { after: "message-bus", initialize(container) { - const banner = Em.Object.create(PreloadStore.get("banner")), site = container.lookup('site:main'); diff --git a/app/assets/javascripts/discourse/initializers/live-development.js.es6 b/app/assets/javascripts/discourse/initializers/live-development.js.es6 index c6dba5fdfcf..de482647b35 100644 --- a/app/assets/javascripts/discourse/initializers/live-development.js.es6 +++ b/app/assets/javascripts/discourse/initializers/live-development.js.es6 @@ -7,6 +7,28 @@ export default { initialize(container) { const messageBus = container.lookup('message-bus:main'); + if (window.history && window.location.search.indexOf("?preview_theme_key=") === 0) { + // force preview theme key to always be carried along + const themeKey = window.location.search.slice(19).split('&')[0]; + if (themeKey.match(/^[a-z0-9-]+$/i)) { + const patchState = function(f) { + const patched = window.history[f]; + + window.history[f] = function(stateObj, name, url) { + if (url.indexOf("preview_theme_key=") === -1) { + const joiner = url.indexOf("?") === -1 ? "?" : "&"; + url = `${url}${joiner}preview_theme_key=${themeKey}`; + } + + return patched.call(window.history, stateObj, name, url); + }; + }; + patchState("replaceState"); + patchState("pushState"); + } + + } + // Custom header changes $('header.custom').each(function() { const header = $(this); diff --git a/app/assets/javascripts/discourse/initializers/localization.js.es6 b/app/assets/javascripts/discourse/initializers/localization.js.es6 index 2603ddaf4bd..e4403e04898 100644 --- a/app/assets/javascripts/discourse/initializers/localization.js.es6 +++ b/app/assets/javascripts/discourse/initializers/localization.js.es6 @@ -4,31 +4,21 @@ export default { name: 'localization', after: 'inject-objects', - enableVerboseLocalization() { - let counter = 0; - let keys = {}; - let t = I18n.t; - I18n.noFallbacks = true; + isVerboseLocalizationEnabled(container) { + const siteSettings = container.lookup('site-settings:main'); + if (siteSettings.verbose_localization) return true; - I18n.t = I18n.translate = function(scope, value){ - let current = keys[scope]; - if (!current) { - current = keys[scope] = ++counter; - let message = "Translation #" + current + ": " + scope; - if (!_.isEmpty(value)) { - message += ", parameters: " + JSON.stringify(value); - } - Em.Logger.info(message); - } - return t.apply(I18n, [scope, value]) + " (#" + current + ")"; - }; + try { + return sessionStorage && sessionStorage.getItem("verbose_localization"); + } catch (e) { + return false; + } }, initialize(container) { - const siteSettings = container.lookup('site-settings:main'); - if (siteSettings.verbose_localization) { - this.enableVerboseLocalization(); + if (this.isVerboseLocalizationEnabled(container)) { + I18n.enableVerboseLocalization(); } // Merge any overrides into our object diff --git a/app/assets/javascripts/discourse/initializers/logout.js.es6 b/app/assets/javascripts/discourse/initializers/logout.js.es6 index dc0ff59c81d..db7d4de9608 100644 --- a/app/assets/javascripts/discourse/initializers/logout.js.es6 +++ b/app/assets/javascripts/discourse/initializers/logout.js.es6 @@ -1,5 +1,7 @@ import logout from 'discourse/lib/logout'; +let _showingLogout = false; + // Subscribe to "logout" change events via the Message Bus export default { name: "logout", @@ -7,14 +9,22 @@ export default { initialize: function (container) { const messageBus = container.lookup('message-bus:main'); - const siteSettings = container.lookup('site-settings:main'); - const keyValueStore = container.lookup('key-value-store:main'); if (!messageBus) { return; } - const callback = () => logout(siteSettings, keyValueStore); messageBus.subscribe("/logout", function () { - bootbox.dialog(I18n.t("logout"), {label: I18n.t("refresh"), callback}, {onEscape: callback, backdrop: 'static'}); + if (!_showingLogout) { + + _showingLogout = true; + + bootbox.dialog(I18n.t("logout"), { + label: I18n.t("refresh"), + callback: logout + }, { + onEscape: logout, + backdrop: 'static' + }); + } }); } }; diff --git a/app/assets/javascripts/discourse/initializers/message-bus.js.es6 b/app/assets/javascripts/discourse/initializers/message-bus.js.es6 index a0b113765cb..11141b4dc02 100644 --- a/app/assets/javascripts/discourse/initializers/message-bus.js.es6 +++ b/app/assets/javascripts/discourse/initializers/message-bus.js.es6 @@ -1,5 +1,20 @@ // Initialize the message bus to receive messages. import pageVisible from 'discourse/lib/page-visible'; +import { handleLogoff } from 'discourse/lib/ajax'; + +function ajax(opts) { + if (opts.complete) { + let oldComplete = opts.complete; + opts.complete = function(xhr, stat) { + handleLogoff(xhr); + oldComplete(xhr, stat); + }; + } else { + opts.complete = handleLogoff; + } + + return $.ajax(opts); +} export default { name: "message-bus", @@ -41,7 +56,7 @@ export default { if (pageVisible()) { opts.headers['Discourse-Visible'] = "true"; } - return $.ajax(opts); + return ajax(opts); }; } else { @@ -50,7 +65,7 @@ export default { if (pageVisible()) { opts.headers['Discourse-Visible'] = "true"; } - return $.ajax(opts); + return ajax(opts); }; messageBus.baseUrl = Discourse.getURL('/'); diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index ec1f3a446dc..acc8573d203 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -1,5 +1,5 @@ import { cleanDOM } from 'discourse/lib/clean-dom'; -import { startPageTracking } from 'discourse/lib/page-tracker'; +import { startPageTracking, googleTagManagerPageChanged } from 'discourse/lib/page-tracker'; import { viewTrackingRequired } from 'discourse/lib/ajax'; export default { @@ -35,15 +35,7 @@ export default { // And Google Tag Manager too if (typeof window.dataLayer !== 'undefined') { - appEvents.on('page:changed', data => { - window.dataLayer.push({ - 'event': 'virtualPageView', - 'page': { - 'title': data.title, - 'url': data.url - } - }); - }); + appEvents.on('page:changed', googleTagManagerPageChanged); } } }; diff --git a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 index d17b2087acd..0cab9b554de 100644 --- a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 +++ b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 @@ -1,13 +1,18 @@ import highlightSyntax from 'discourse/lib/highlight-syntax'; import lightbox from 'discourse/lib/lightbox'; +import { setTextDirections } from "discourse/lib/text-direction"; import { withPluginApi } from 'discourse/lib/plugin-api'; export default { name: "post-decorations", - initialize() { + initialize(container) { withPluginApi('0.1', api => { + const siteSettings = container.lookup('site-settings:main'); api.decorateCooked(highlightSyntax); api.decorateCooked(lightbox); + if (siteSettings.support_mixed_text_direction) { + api.decorateCooked(setTextDirections); + } api.decorateCooked($elem => { const players = $('audio', $elem); diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 index 7c16106ddae..8632ced10ee 100644 --- a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 +++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 @@ -2,11 +2,26 @@ export default { name: 'register-service-worker', initialize() { - const isSecure = (document.location.protocol === 'https:') || - (location.hostname === "localhost"); + const isSecured = (document.location.protocol === 'https:') || + (location.hostname === "localhost"); - if (isSecure && ('serviceWorker' in navigator)) { - navigator.serviceWorker.register(`${Discourse.BaseUri}/service-worker.js`); + const isSupported= isSecured && ('serviceWorker' in navigator); + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + if (isSupported) { + if (Discourse.ServiceWorkerURL && !isSafari) { + navigator.serviceWorker + .register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`) + .catch(error => { + Ember.Logger.info(`Failed to register Service Worker: ${error}`); + }); + } else { + navigator.serviceWorker.getRegistrations().then(registrations => { + for(let registration of registrations) { + registration.unregister(); + }; + }); + } } } }; diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index a477463d953..75fe71d1d78 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -2,8 +2,14 @@ import { init as initDesktopNotifications, onNotification, - alertChannel + alertChannel, + disable as disableDesktopNotifications, } from 'discourse/lib/desktop-notifications'; +import { + register as registerPushNotifications, + unsubscribe as unsubscribePushNotifications, + isPushNotificationsEnabled +} from 'discourse/lib/push-notifications'; export default { name: 'subscribe-user-notifications', @@ -11,14 +17,9 @@ export default { initialize(container) { const user = container.lookup('current-user:main'); - const keyValueStore = container.lookup('key-value-store:main'); const bus = container.lookup('message-bus:main'); const appEvents = container.lookup('app-events:main'); - // clear old cached notifications, we used to store in local storage - // TODO 2017 delete this line - keyValueStore.remove('recent-notifications'); - if (user) { if (user.get('staff')) { bus.subscribe('/flagged_counts', data => { @@ -34,7 +35,6 @@ export default { bus.subscribe(`/notification/${user.get('id')}`, data => { const store = container.lookup('service:store'); - const oldUnread = user.get('unread_notifications'); const oldPM = user.get('unread_private_messages'); @@ -65,7 +65,7 @@ export default { oldNotifications.insertAt(insertPosition, Em.Object.create(lastNotification)); } - for (let idx=0; idx < data.recent.length; idx++) { + for (let idx = 0; idx < data.recent.length; idx++) { let old; while(old = oldNotifications[idx]) { const info = data.recent[idx]; @@ -87,6 +87,7 @@ export default { const site = container.lookup('site:main'); const siteSettings = container.lookup('site-settings:main'); + const router = container.lookup('router:main'); bus.subscribe("/categories", data => { _.each(data.categories, c => site.updateCategory(c)); @@ -94,15 +95,17 @@ export default { }); bus.subscribe("/client_settings", data => Ember.set(siteSettings, data.name, data.value)); - - bus.subscribe("/refresh_client", data => { - Discourse.set("assetVersion", data); - }); + bus.subscribe("/refresh_client", data => Discourse.set("assetVersion", data)); if (!Ember.testing) { - if (!site.mobileView) { - bus.subscribe(alertChannel(user), data => onNotification(data, user)); - initDesktopNotifications(bus, appEvents); + bus.subscribe(alertChannel(user), data => onNotification(data, user)); + initDesktopNotifications(bus, appEvents); + + if (isPushNotificationsEnabled(user, site.mobileView)) { + disableDesktopNotifications(); + registerPushNotifications(Discourse.User.current(), site.mobileView, router, appEvents); + } else { + unsubscribePushNotifications(user); } } } diff --git a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 index afe7dbe6bd8..9b952941d15 100644 --- a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 @@ -21,7 +21,7 @@ export default { } DiscourseURL.rewrite(/^\/u\/([^\/]+)\/?$/, "/u/$1/summary", { - exceptions: ['/u/account-created', '/users/account-created'] + exceptions: ['/u/account-created', '/users/account-created', '/u/password-reset', '/users/password-reset'] }); } }; diff --git a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 index bb2a915727f..9666d9c378f 100644 --- a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 +++ b/app/assets/javascripts/discourse/lib/ajax-error.js.es6 @@ -54,5 +54,11 @@ export function throwAjaxError(undoCallback) { } export function popupAjaxError(error) { + if (error && error._discourse_displayed) { return; } bootbox.alert(extractError(error)); + + error._discourse_displayed = true; + + // We re-throw in a catch to not swallow the exception + throw error; } diff --git a/app/assets/javascripts/discourse/lib/ajax.js.es6 b/app/assets/javascripts/discourse/lib/ajax.js.es6 index 98e65ed9c08..15e66b3f042 100644 --- a/app/assets/javascripts/discourse/lib/ajax.js.es6 +++ b/app/assets/javascripts/discourse/lib/ajax.js.es6 @@ -1,7 +1,9 @@ import pageVisible from 'discourse/lib/page-visible'; +import logout from 'discourse/lib/logout'; let _trackView = false; let _transientHeader = null; +let _showingLogout = false; export function setTransientHeader(key, value) { _transientHeader = {key, value}; @@ -11,6 +13,22 @@ export function viewTrackingRequired() { _trackView = true; } +export function handleLogoff(xhr) { + if (xhr.getResponseHeader('Discourse-Logged-Out') && !_showingLogout) { + _showingLogout = true; + const messageBus = Discourse.__container__.lookup('message-bus:main'); + messageBus.stop(); + bootbox.dialog( + I18n.t("logout"), {label: I18n.t("refresh"), callback: logout}, + { + onEscape: () => logout(), + backdrop: 'static' + } + ); + } +}; + + /** Our own $.ajax method. Makes sure the .then method executes in an Ember runloop for performance reasons. Also automatically adjusts the URL to support installs @@ -39,6 +57,10 @@ export function ajax() { args.headers = args.headers || {}; + if (Discourse.__container__.lookup('current-user:main')) { + args.headers['Discourse-Logged-In'] = "true"; + } + if (_transientHeader) { args.headers[_transientHeader.key] = _transientHeader.value; _transientHeader = null; @@ -55,6 +77,8 @@ export function ajax() { } args.success = (data, textStatus, xhr) => { + handleLogoff(xhr); + if (xhr.getResponseHeader('Discourse-Readonly')) { Ember.run(() => Discourse.Site.currentProp('isReadOnly', true)); } @@ -67,6 +91,8 @@ export function ajax() { }; args.error = (xhr, textStatus, errorThrown) => { + handleLogoff(xhr); + // note: for bad CSRF we don't loop an extra request right away. // this allows us to eliminate the possibility of having a loop. if (xhr.status === 403 && xhr.responseText === "[\"BAD CSRF\"]") { @@ -110,7 +136,7 @@ export function ajax() { if(args.type && args.type.toUpperCase() !== 'GET' && !Discourse.Session.currentProp('csrfToken')){ promise = new Ember.RSVP.Promise((resolve, reject) => { ajaxObj = $.ajax(Discourse.getURL('/session/csrf'), {cache: false}) - .success(result => { + .done(result => { Discourse.Session.currentProp('csrfToken', result.csrf); performAjax(resolve, reject); }); diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 15cb6d5d937..cf0a9a01d6f 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -276,7 +276,6 @@ export default function(options) { } if (Discourse.Site.currentProp('mobileView')) { - div.css('width', 'auto'); if ((me.height() / 2) >= pos.top) { vOffset = -23; } if ((me.width() / 2) <= pos.left) { hOffset = -div.width(); } diff --git a/app/assets/javascripts/discourse/lib/binary-search.js.es6 b/app/assets/javascripts/discourse/lib/binary-search.js.es6 deleted file mode 100644 index 03675866e0a..00000000000 --- a/app/assets/javascripts/discourse/lib/binary-search.js.es6 +++ /dev/null @@ -1,29 +0,0 @@ -// The binarySearch() function is licensed under the UNLICENSE -// https://github.com/Olical/binary-search - -// Modified for use in Discourse - -export default function binarySearch(list, target, keyProp) { - var min = 0; - var max = list.length - 1; - var guess; - var keyProperty = keyProp || "id"; - - while (min <= max) { - guess = Math.floor((min + max) / 2); - - if (Em.get(list[guess], keyProperty) === target) { - return guess; - } - else { - if (Em.get(list[guess], keyProperty) < target) { - min = guess + 1; - } - else { - max = guess - 1; - } - } - } - - return -Math.floor((min + max) / 2); -} diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 6247973d451..d4dc44f2cbb 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -26,7 +26,7 @@ export default { } // don't track links in quotes or in elided part - let tracking = $link.parents('aside.quote,.elided').length === 0; + let tracking = $link.parents('aside.quote, .elided').length === 0; let href = $link.attr('href') || $link.data('href'); @@ -113,8 +113,10 @@ export default { return false; } + const isInternal = DiscourseURL.isInternal(href); + // If we're on the same site, use the router and track via AJAX - if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { + if (tracking && isInternal && !$link.hasClass('attachment')) { ajax("/clicks/track", { data: { url: href, @@ -128,8 +130,11 @@ export default { return false; } - // Otherwise, use a custom URL with a redirect - if (Discourse.User.currentProp('external_links_in_new_tab')) { + const modifierLeftClicked = (e.ctrlKey || e.metaKey) && e.which === 1; + const middleClicked = e.which === 2; + const openExternalInNewTab = Discourse.User.currentProp('external_links_in_new_tab'); + + if (modifierLeftClicked || middleClicked || (!isInternal && openExternalInNewTab)) { window.open(destUrl, '_blank').focus(); } else { DiscourseURL.redirectTo(destUrl); diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 index 12c481e4afb..d07442d24c6 100644 --- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 +++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 @@ -55,6 +55,22 @@ function init(messageBus, appEvents) { } } +function confirmNotification() { + const notification = new Notification(I18n.t("notifications.popup.confirm_title", {site_title: Discourse.SiteSettings.title}), { + body: I18n.t("notifications.popup.confirm_body"), + icon: Discourse.SiteSettings.logo_small_url || Discourse.SiteSettings.logo_url, + tag: "confirm-subscription" + }); + + const clickEventHandler = () => notification.close(); + + notification.addEventListener('click', clickEventHandler); + setTimeout(() => { + notification.close(); + notification.removeEventListener('click', clickEventHandler); + }, 10 * 1000); +} + // This function is only called if permission was granted function setupNotifications(appEvents) { @@ -167,4 +183,8 @@ function unsubscribe(bus, user) { bus.unsubscribe(alertChannel(user)); } -export { context, init, onNotification, unsubscribe, alertChannel }; +function disable() { + keyValueStore.setItem('notifications-disabled', 'disabled'); +} + +export { context, init, onNotification, unsubscribe, alertChannel, confirmNotification, disable }; diff --git a/app/assets/javascripts/discourse/lib/formatter.js.es6 b/app/assets/javascripts/discourse/lib/formatter.js.es6 index dcce95ca999..3eed7e3d97f 100644 --- a/app/assets/javascripts/discourse/lib/formatter.js.es6 +++ b/app/assets/javascripts/discourse/lib/formatter.js.es6 @@ -301,9 +301,13 @@ export function relativeAge(date, options) { return "UNKNOWN FORMAT"; } -export function number(val) { +export function number(val, options = {}) { let formattedNumber; + if (options.ceil) { + val = Math.ceil(val); + } + val = parseInt(val, 10); if (isNaN(val)) val = 0; @@ -311,7 +315,7 @@ export function number(val) { formattedNumber = I18n.toNumber(val / 1000000, {precision: 1}); return I18n.t("number.short.millions", {number: formattedNumber}); } else if (val > 99999) { - formattedNumber = I18n.toNumber(val / 1000, {precision: 0}); + formattedNumber = I18n.toNumber(Math.floor(val / 1000), {precision: 0}); return I18n.t("number.short.thousands", {number: formattedNumber}); } else if (val > 999) { formattedNumber = I18n.toNumber(val / 1000, {precision: 1}); diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index c3a7ecd27ca..6a1ddbdf9f1 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -1,6 +1,6 @@ import DiscourseURL from 'discourse/lib/url'; import Composer from 'discourse/models/composer'; -import { scrollTopFor } from 'discourse/lib/offset-calculator'; +import { minimumOffset } from 'discourse/lib/offset-calculator'; const bindings = { '!': {postAction: 'showFlags'}, @@ -12,6 +12,7 @@ const bindings = { '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics 'b': {handler: 'toggleBookmark'}, 'c': {handler: 'createTopic'}, + 'C': {handler: 'focusComposer'}, 'ctrl+f': {handler: 'showPageSearch', anonymous: true}, 'command+f': {handler: 'showPageSearch', anonymous: true}, 'ctrl+p': {handler: 'printTopic', anonymous: true}, @@ -67,6 +68,12 @@ export default { this.searchService = this.container.lookup('search-service:main'); this.appEvents = this.container.lookup('app-events:main'); this.currentUser = this.container.lookup('current-user:main'); + let siteSettings = this.container.lookup('site-settings:main'); + + // Disable the shortcut if private messages are disabled + if (!siteSettings.enable_personal_messages) { + delete bindings['g m']; + } Object.keys(bindings).forEach(key => { const binding = bindings[key]; @@ -174,6 +181,15 @@ export default { } }, + focusComposer() { + const composer = this.container.lookup('controller:composer'); + if (composer.get('model.viewOpen')) { + setTimeout(() => $('textarea.d-editor-input').focus(), 0); + } else { + composer.send('openIfDraft'); + } + }, + pinUnpinTopic() { this.container.lookup('controller:topic').togglePinnedState(); }, @@ -295,52 +311,36 @@ export default { _moveSelection(direction) { const $articles = this._findArticles(); - if (typeof $articles === 'undefined') { - return; - } + if (typeof $articles === 'undefined') return; const $selected = ($articles.filter('.selected').length !== 0) ? $articles.filter('.selected') : $articles.filter('[data-islastviewedtopic=true]'); + let index = $articles.index($selected); - if ($selected.length !== 0) { //boundries check - // loop is not allowed - if (direction === -1 && index === 0) { return; } - if (direction === 1 && index === ($articles.size()-1) ) { return; } + if ($selected.length !== 0) { + if (direction === -1 && index === 0) return; + if (direction === 1 && index === $articles.length - 1) return; } - // if nothing is selected go to the first post on screen + // when nothing is selected if ($selected.length === 0) { - const scrollTop = $(document).scrollTop(); - - index = 0; - $articles.each(function() { - const top = $(this).position().top; - if (top >= scrollTop) { - return false; - } - index += 1; - }); - - if (index >= $articles.length) { - index = $articles.length - 1; - } - + // select the first post with its top visible + const offset = minimumOffset(); + index = $articles.toArray().findIndex(article => article.getBoundingClientRect().top > offset); direction = 0; } const $article = $articles.eq(index + direction); - if ($article.size() > 0) { - + if ($article.length > 0) { $articles.removeClass('selected'); $article.addClass('selected'); if ($article.is('.topic-post')) { $('a.tabLoc', $article).focus(); this._scrollToPost($article); - } else { this._scrollList($article, direction); } @@ -348,8 +348,11 @@ export default { }, _scrollToPost($article) { - const pos = $article.offset(); - $(window).scrollTop(Math.ceil(pos.top - scrollTopFor(pos.top))); + if ($article.find("#post_1").length > 0) { + $(window).scrollTop(0); + } else { + $(window).scrollTop($article.offset().top - minimumOffset()); + } }, _scrollList($article) { @@ -377,14 +380,13 @@ export default { _findArticles() { - const $topicList = $('.topic-list'), - $topicArea = $('.posts-wrapper'); + const $topicList = $(".topic-list"); + const $postsWrapper = $(".posts-wrapper"); - if ($topicArea.size() > 0) { - return $('.posts-wrapper .topic-post, .topic-list tbody tr'); - } - else if ($topicList.size() > 0) { - return $topicList.find('.topic-list-item'); + if ($postsWrapper.length > 0) { + return $(".posts-wrapper .topic-post, .topic-list tbody tr"); + } else if ($topicList.length > 0) { + return $topicList.find(".topic-list-item"); } }, diff --git a/app/assets/javascripts/discourse/lib/lock-on.js.es6 b/app/assets/javascripts/discourse/lib/lock-on.js.es6 index 65504d4df67..fe20031ab3a 100644 --- a/app/assets/javascripts/discourse/lib/lock-on.js.es6 +++ b/app/assets/javascripts/discourse/lib/lock-on.js.es6 @@ -1,13 +1,13 @@ -import { scrollTopFor } from 'discourse/lib/offset-calculator'; +import { minimumOffset } from "discourse/lib/offset-calculator"; -// Dear traveller, you are entering a zone where we are at war with the browser -// the browser is insisting on positioning scrollTop per the location it was in -// the past, we are insisting on it being where we want it to be -// The hack is just to keep trying over and over to position the scrollbar (up to 1 minute) +// Dear traveller, you are entering a zone where we are at war with the browser. +// The browser is insisting on positioning scrollTop per the location it was in +// the past, we are insisting on it being where we want it to be. +// The hack is just to keep trying over and over to position the scrollbar (up to 1 second). // // The root cause is that a "refresh" on a topic page will almost never be at the // same position it was in the past, the URL points to the post at the top of the -// page, so a refresh will try to bring that post into view causing drift +// page, so a refresh will try to bring that post into view causing drift. // // Additionally if you loaded multiple batches of posts, on refresh they will not // be loaded. @@ -18,29 +18,29 @@ import { scrollTopFor } from 'discourse/lib/offset-calculator'; // 1. onbeforeunload ensure we are scrolled to the right spot // 2. give up on the scrollbar and implement it ourselves (something that will happen) +const LOCK_DURATION_MS = 1000; const SCROLL_EVENTS = "scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on"; +const SCROLL_TYPES = ["mousedown", "mousewheel", "touchmove", "wheel"]; function within(threshold, x, y) { - return Math.abs(x-y) < threshold; + return Math.abs(x - y) < threshold; } export default class LockOn { constructor(selector, options) { this.selector = selector; this.options = options || {}; - this.offsetTop = null; } elementTop() { - const selected = $(this.selector); - if (selected && selected.offset && selected.offset()) { - const result = selected.offset().top; - return result - (Math.round(scrollTopFor(result)) * 0.9); + const $selected = $(this.selector); + if ($selected && $selected.offset && $selected.offset()) { + return $selected.offset().top - minimumOffset(); } } clearLock(interval) { - $('body,html').off(SCROLL_EVENTS); + $("body, html").off(SCROLL_EVENTS); clearInterval(interval); if (this.options.finished) { this.options.finished(); @@ -54,9 +54,7 @@ export default class LockOn { $(window).scrollTop(previousTop); const interval = setInterval(() => { - let top = this.elementTop(); - if (top < 0) { top = 0; } - + const top = Math.max(0, this.elementTop()); const scrollTop = $(window).scrollTop(); if (typeof(top) === "undefined" || isNaN(top)) { @@ -68,20 +66,14 @@ export default class LockOn { previousTop = top; } - // We commit suicide after 3s just to clean up - const nowTime = new Date().getTime(); - if (nowTime - startedAt > 1000) { + // Commit suicide after a little while + if (new Date().getTime() - startedAt > LOCK_DURATION_MS) { return this.clearLock(interval); } }, 50); - $('body,html').off(SCROLL_EVENTS).on(SCROLL_EVENTS, e => { - if ( e.which > 0 || - e.type === "mousedown" || - e.type === "mousewheel" || - e.type === "touchmove" || - e.type === "wheel" - ) { + $("body, html").off(SCROLL_EVENTS).on(SCROLL_EVENTS, e => { + if (e.which > 0 || SCROLL_TYPES.includes(e.type)) { this.clearLock(interval); } }); diff --git a/app/assets/javascripts/discourse/lib/logout.js.es6 b/app/assets/javascripts/discourse/lib/logout.js.es6 index 2fe4078a69f..51f397d186b 100644 --- a/app/assets/javascripts/discourse/lib/logout.js.es6 +++ b/app/assets/javascripts/discourse/lib/logout.js.es6 @@ -1,4 +1,10 @@ export default function logout(siteSettings, keyValueStore) { + if (!siteSettings || !keyValueStore) { + const container = Discourse.__container__; + siteSettings = siteSettings || container.lookup('site-settings:main'); + keyValueStore = keyValueStore || container.lookup('key-value-store:main'); + } + keyValueStore.abandonLocal(); const redirect = siteSettings.logout_redirect; diff --git a/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 b/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 index 84691d6a05d..41d50903bcb 100644 --- a/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 +++ b/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 @@ -1,49 +1,36 @@ -// TODO: This is quite ugly but seems reasonably fast? Maybe refactor -// this out before we merge into stable. export function scrollTopFor(y) { - let off = 0; - for (let i=0; i<3; i++) { - off = offsetCalculator(y - off); - } - return off; + return y - offsetCalculator(); } -export default function offsetCalculator(y) { - const $header = $('header'); - const $title = $('#topic-title'); - const rawWinHeight = $(window).height(); - const windowHeight = rawWinHeight - $title.height(); - const eyeTarget = (windowHeight / 10); - const headerHeight = $header.outerHeight(true); - const expectedOffset = $title.height() - $header.find('.contents').height() + (eyeTarget * 2); - const ideal = headerHeight + ((expectedOffset < 0) ? 0 : expectedOffset); - - const $container = $('.posts-wrapper'); - if ($container.length === 0) { return expectedOffset; } - - const topPos = $container.offset().top; - - const scrollTop = y || $(window).scrollTop(); - const docHeight = $(document).height(); - let scrollPercent = Math.min((scrollTop / (docHeight-rawWinHeight)), 1.0); - - let inter = topPos - scrollTop + ($container.height() * scrollPercent); - if (inter < headerHeight + eyeTarget) { - inter = headerHeight + eyeTarget; - } - - - if (inter > ideal) { - const bottom = $('#topic-bottom').offset().top; - const switchPos = bottom - rawWinHeight - ideal; - - if (scrollTop > switchPos) { - const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0); - return ((1 - p) * ideal) + (p * inter); - } else { - return ideal; - } - } - - return inter; +export function minimumOffset() { + const $header = $("header.d-header"); + const headerHeight = $header.outerHeight(true) || 0; + const headerPositionTop = $header.position().top; + return headerHeight + headerPositionTop; +} + +export default function offsetCalculator() { + const min = minimumOffset(); + + // on mobile, just use the header + if ($("html").hasClass("mobile-view")) return min; + + const $window = $(window); + const windowHeight = $window.height(); + const documentHeight = $(document).height(); + const topicBottomOffsetTop = $("#topic-bottom").offset().top; + + // the footer is bigger than the window, we can scroll down past the last post + if (documentHeight - windowHeight > topicBottomOffsetTop) return min; + + const scrollTop = $window.scrollTop(); + const visibleBottomHeight = scrollTop + windowHeight - topicBottomOffsetTop; + + if (visibleBottomHeight > 0) { + const bottomHeight = documentHeight - topicBottomOffsetTop; + const offset = (windowHeight - bottomHeight) * visibleBottomHeight / bottomHeight; + return Math.max(min, offset); + } + + return min; } diff --git a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 b/app/assets/javascripts/discourse/lib/page-tracker.js.es6 index e6bd2c79e1b..77daeedce1a 100644 --- a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 +++ b/app/assets/javascripts/discourse/lib/page-tracker.js.es6 @@ -38,3 +38,23 @@ export function startPageTracking(router, appEvents) { }); _started = true; } + +const _gtmPageChangedCallbacks = []; + +export function addGTMPageChangedCallback(callback) { + _gtmPageChangedCallbacks.push(callback); +} + +export function googleTagManagerPageChanged(data) { + let gtmData = { + 'event': 'virtualPageView', + 'page': { + 'title': data.title, + 'url': data.url + } + }; + + _.each(_gtmPageChangedCallbacks, callback => callback(gtmData)); + + window.dataLayer.push(gtmData); +} diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 68cc7eb8e61..3222c2ddd26 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -22,9 +22,13 @@ import { registerIconRenderer, replaceIcon } from 'discourse-common/lib/icon-lib import { addNavItem } from 'discourse/models/nav-item'; import { replaceFormatter } from 'discourse/lib/utilities'; import { modifySelectKit } from "select-kit/mixins/plugin-api"; +import { addGTMPageChangedCallback } from 'discourse/lib/page-tracker'; +import { registerCustomAvatarHelper } from 'discourse/helpers/user-avatar'; +import { disableNameSuppression } from 'discourse/widgets/poster-name'; +import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from 'discourse/controllers/topic'; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = '0.8.13'; +const PLUGIN_API_VERSION = '0.8.22'; class PluginApi { constructor(version, container) { @@ -41,20 +45,7 @@ class PluginApi { return this.container.lookup('current-user:main'); } - /** - * Allows you to overwrite or extend methods in a class. - * - * For example: - * - * ``` - * api.modifyClass('controller:composer', { - * actions: { - * newActionHere() { } - * } - * }); - * ``` - **/ - modifyClass(resolverName, changes, opts) { + _resolveClass(resolverName, opts) { opts = opts || {}; if (this.container.cache[resolverName]) { @@ -69,7 +60,48 @@ class PluginApi { return; } - klass.class.reopen(changes); + return klass; + } + + /** + * Allows you to overwrite or extend methods in a class. + * + * For example: + * + * ``` + * api.modifyClass('controller:composer', { + * actions: { + * newActionHere() { } + * } + * }); + * ``` + **/ + modifyClass(resolverName, changes, opts) { + + const klass = this._resolveClass(resolverName, opts); + if (klass) { + klass.class.reopen(changes); + } + return klass; + } + + /** + * Allows you to overwrite or extend static methods in a class. + * + * For example: + * + * ``` + * api.modifyClassStatic('controller:composer', { + * superFinder: function() { return []; } + * }); + * ``` + **/ + modifyClassStatic(resolverName, changes, opts) { + + const klass = this._resolveClass(resolverName, opts); + if (klass) { + klass.class.reopenClass(changes); + } return klass; } @@ -350,8 +382,67 @@ class PluginApi { ``` **/ onPageChange(fn) { + this.onAppEvent('page:changed', data => fn(data.url, data.title)); + } + + /** + Listen for a triggered `AppEvent` from Discourse. + + ```javascript + api.onAppEvent('inserted-custom-html', () => { + console.log('a custom footer was rendered'); + }); + ``` + **/ + onAppEvent(name, fn) { let appEvents = this.container.lookup('app-events:main'); - appEvents.on('page:changed', data => fn(data.url, data.title)); + appEvents.on(name, fn); + } + + /** + Registers a function to generate custom avatar CSS classes + for a particular user. + + Takes a function that will accept a user as a parameter + and return an array of CSS classes to apply. + + ```javascript + api.customUserAvatarClasses(user => { + if (Ember.get(user, 'primary_group_name') === 'managers') { + return ['managers']; + } + }); + **/ + customUserAvatarClasses(fn) { + registerCustomAvatarHelper(fn); + } + + /** + * Allows you to disable suppression of similar username / names on posts + * If a user has the username bob.bob and the name Bob Bob, one of the two + * will be suppressed depending on prioritize_username_in_ux. + * This allows you to override core behavior + **/ + disableNameSuppressionOnPosts() { + disableNameSuppression(); + } + + /** + * Registers a callback that will be invoked when the server calls + * Post#publish_change_to_clients! please ensure your type does not + * match acted,revised,rebaked,recovered, created,move_to_inbox or archived + * + * callback will be called with topicController and Message + * + * Example: + * + * api.registerCustomPostMessageCallback("applied_color", (topicController, message) => { + * let stream = topicController.get("model.postStream"); + * // etc + * }); + */ + registerCustomPostMessageCallback(type, callback) { + registerCustomPostMessageCallback1(type, callback); } /** @@ -605,6 +696,20 @@ class PluginApi { modifySelectKit(pluginApiKey) { return modifySelectKit(pluginApiKey); } + + /** + * + * Registers a function that can inspect and modify the data that + * will be sent to Google Tag Manager when a page changed event is triggered. + * + * Example: + * + * addGTMPageChangedCallback( gtmData => gtmData.locale = I18n.currentLocale() ) + * + */ + addGTMPageChangedCallback(fn) { + addGTMPageChangedCallback(fn); + } } let _pluginv01; @@ -665,7 +770,12 @@ export function withPluginApi(version, apiCodeCallback, opts) { let _decorateId = 0; function decorate(klass, evt, cb) { const mixin = {}; - mixin["_decorate_" + (_decorateId++)] = function($elem) { cb($elem); }.on(evt); + mixin["_decorate_" + (_decorateId++)] = function($elem) { + $elem = $elem || this.$(); + if ($elem) { + cb($elem); + } + }.on(evt); klass.reopen(mixin); } diff --git a/app/assets/javascripts/discourse/lib/push-notifications.js.es6 b/app/assets/javascripts/discourse/lib/push-notifications.js.es6 new file mode 100644 index 00000000000..53976b1cba7 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/push-notifications.js.es6 @@ -0,0 +1,119 @@ +import { ajax } from 'discourse/lib/ajax'; +import KeyValueStore from 'discourse/lib/key-value-store'; + +export const keyValueStore = new KeyValueStore("discourse_push_notifications_"); + +export function userSubscriptionKey(user) { + return `subscribed-${user.get('id')}`; +} + +function sendSubscriptionToServer(subscription, sendConfirmation) { + ajax('/push_notifications/subscribe', { + type: 'POST', + data: { subscription: subscription.toJSON(), send_confirmation: sendConfirmation } + }); +} + +function userAgentVersionChecker(agent, version, mobileView) { + const uaMatch = navigator.userAgent.match(new RegExp(`${agent}\/(\\d+)\\.\\d`)); + if (uaMatch && mobileView) return false; + if (!uaMatch || parseInt(uaMatch[1]) < version) return false; + return true; +} + +function resetIdle() { + if('controller' in navigator.serviceWorker && navigator.serviceWorker.controller != null) { + navigator.serviceWorker.controller.postMessage({lastAction: Date.now()}); + } +} + +function setupActivityListeners(appEvents) { + window.addEventListener("focus", resetIdle); + + if (document) { + document.addEventListener("scroll", resetIdle); + } + + appEvents.on('page:changed', resetIdle); +} + +export function isPushNotificationsSupported(mobileView) { + if (!(('serviceWorker' in navigator) && + (ServiceWorkerRegistration && + (typeof(Notification) !== "undefined") && + ('showNotification' in ServiceWorkerRegistration.prototype) && + ('PushManager' in window)))) { + + return false; + } + + if ((!userAgentVersionChecker('Firefox', 44, mobileView)) && + (!userAgentVersionChecker('Chrome', 50))) { + return false; + } + + return true; +} + +export function isPushNotificationsEnabled(user, mobileView) { + return user && isPushNotificationsSupported(mobileView) && keyValueStore.getItem(userSubscriptionKey(user)); +} + +export function register(user, mobileView, router, appEvents) { + if (!isPushNotificationsSupported(mobileView)) return; + if (Notification.permission === 'denied' || !user) return; + + navigator.serviceWorker.ready.then(serviceWorkerRegistration => { + serviceWorkerRegistration.pushManager.getSubscription().then(subscription => { + if (subscription) { + sendSubscriptionToServer(subscription, false); + // Resync localStorage + keyValueStore.setItem(userSubscriptionKey(user), 'subscribed'); + } + setupActivityListeners(appEvents); + }).catch(e => Ember.Logger.error(e)); + }); + + navigator.serviceWorker.addEventListener('message', (event) => { + if ('url' in event.data) { + const url = event.data.url; + router.handleURL(url); + } + }); +} + +export function subscribe(callback, applicationServerKey, mobileView) { + if (!isPushNotificationsSupported(mobileView)) return; + + navigator.serviceWorker.ready.then(serviceWorkerRegistration => { + serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: new Uint8Array(applicationServerKey.split("|")) // eslint-disable-line no-undef + }).then(subscription => { + sendSubscriptionToServer(subscription, true); + if (callback) callback(); + }).catch(e => Ember.Logger.error(e)); + }); +} + +export function unsubscribe(user, callback, mobileView) { + if (!isPushNotificationsSupported(mobileView)) return; + + keyValueStore.setItem(userSubscriptionKey(user), ''); + navigator.serviceWorker.ready.then(serviceWorkerRegistration => { + serviceWorkerRegistration.pushManager.getSubscription().then(subscription => { + if (subscription) { + subscription.unsubscribe().then((successful) => { + if (successful) { + ajax('/push_notifications/unsubscribe', { + type: 'POST', + data: { subscription: subscription.toJSON() } + }); + } + }); + } + }).catch(e => Ember.Logger.error(e)); + + if (callback) callback(); + }); +} diff --git a/app/assets/javascripts/discourse/lib/quote.js.es6 b/app/assets/javascripts/discourse/lib/quote.js.es6 index bf1d290a5f0..8dee8809818 100644 --- a/app/assets/javascripts/discourse/lib/quote.js.es6 +++ b/app/assets/javascripts/discourse/lib/quote.js.es6 @@ -4,41 +4,32 @@ export default { // Build the BBCode quote around the selected text build(post, contents, opts) { - var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp; - var full = opts && opts["full"]; - var raw = opts && opts["raw"]; - if (!post) { return ""; } if (!contents) contents = ""; - sansQuotes = contents.replace(this.REGEXP, '').trim(); + const sansQuotes = contents.replace(this.REGEXP, "").trim(); if (sansQuotes.length === 0) { return ""; } - // Escape the content of the quote - sansQuotes = sansQuotes.replace(//g, ">"); + // Strip the HTML from cooked + const stripped = $("
      ").html(post.get("cooked")).text(); - result = "[quote=\"" + post.get('username') + ", post:" + post.get('post_number') + ", topic:" + post.get('topic_id'); + // Let's remove any non-word characters as a kind of hash. + // Yes it's not accurate but it should work almost every time we need it to. + // It would be unlikely that the user would quote another post that matches in exactly this way. + const sameContent = stripped.replace(/\W/g, "") === contents.replace(/\W/g, ""); - /* Strip the HTML from cooked */ - tmp = document.createElement('div'); - tmp.innerHTML = post.get('cooked'); - stripped = tmp.textContent || tmp.innerText || ""; + const params = [ + post.get("username"), + `post:${post.get("post_number")}`, + `topic:${post.get("topic_id")}`, + ]; - /* - Let's remove any non alphanumeric characters as a kind of hash. Yes it's - not accurate but it should work almost every time we need it to. It would be unlikely - that the user would quote another post that matches in exactly this way. - */ - stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, ''); - contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, ''); + opts = opts || {}; - /* If the quote is the full message, attribute it as such */ - if (full || stripped_hashed === contents_hashed) result += ", full:true"; - result += "\"]\n" + (raw ? contents : sansQuotes) + "\n[/quote]\n\n"; + if (opts["full"] || sameContent) params.push("full:true"); - return result; + return `[quote="${params.join(", ")}"]\n${opts["raw"] ? contents : sansQuotes}\n[/quote]\n\n`; } }; diff --git a/app/assets/javascripts/discourse/lib/render-tag.js.es6 b/app/assets/javascripts/discourse/lib/render-tag.js.es6 index 127dc5c9fe4..b9763676f40 100644 --- a/app/assets/javascripts/discourse/lib/render-tag.js.es6 +++ b/app/assets/javascripts/discourse/lib/render-tag.js.es6 @@ -3,7 +3,16 @@ export default function renderTag(tag, params) { tag = Handlebars.Utils.escapeExpression(tag); const classes = ['tag-' + tag, 'discourse-tag']; const tagName = params.tagName || "a"; - const href = (tagName === "a" && !params.noHref) ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : ""; + let path; + if (tagName === "a" && !params.noHref) { + if (params.isPrivateMessage && Discourse.User.current()) { + const username = params.tagsForUser ? params.tagsForUser : Discourse.User.current().username; + path = `/u/${username}/messages/tags/${tag}`; + } else { + path = `/tags/${tag}`; + } + } + const href = path ? ` href='${Discourse.getURL(path)}' ` : ""; if (Discourse.SiteSettings.tag_style || params.style) { classes.push(params.style || Discourse.SiteSettings.tag_style); diff --git a/app/assets/javascripts/discourse/lib/render-tags.js.es6 b/app/assets/javascripts/discourse/lib/render-tags.js.es6 index 6989eab57b1..f6bb19b92e4 100644 --- a/app/assets/javascripts/discourse/lib/render-tags.js.es6 +++ b/app/assets/javascripts/discourse/lib/render-tags.js.es6 @@ -20,9 +20,16 @@ export function addTagsHtmlCallback(callback, options) { export default function(topic, params){ let tags = topic.tags; let buffer = ""; + let tagsForUser = null; + const isPrivateMessage = topic.get('isPrivateMessage'); - if (params && params.mode === "list") { - tags = topic.get("visibleListTags"); + if (params) { + if (params.mode === "list") { + tags = topic.get("visibleListTags"); + } + if (params.tagsForUser) { + tagsForUser = params.tagsForUser; + } } let customHtml = null; @@ -43,7 +50,7 @@ export default function(topic, params){ buffer = "
      "; if (tags) { for(let i=0; i { const error = e.jqXHR; if (error.status === 405 && error.responseJSON.error_type === "read_only") return; + }).finally(() => { + this._inProgress = false; + this._lastFlush = 0; }); } else if (this._anonCallback) { // Anonymous viewer - save to localStorage @@ -182,7 +187,7 @@ export default class { return timings[postNumber] > 0 && !totalTimings[postNumber]; }); - if (this._lastFlush > nextFlush || rush) { + if (!this._inProgress && (this._lastFlush > nextFlush || rush)) { this.flush(); } diff --git a/app/assets/javascripts/discourse/lib/text-direction.js.es6 b/app/assets/javascripts/discourse/lib/text-direction.js.es6 new file mode 100644 index 00000000000..928b147ed79 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/text-direction.js.es6 @@ -0,0 +1,30 @@ +const ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF'; +const rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; +const rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']'); +const ltrDirCheck = new RegExp('^[^'+rtlChars+']*['+ltrChars+']'); +let _siteDir; + +export function isRTL(text) { + return rtlDirCheck.test(text); +} + +export function isLTR(text) { + return ltrDirCheck.test(text); +} + +export function setTextDirections($elem) { + $elem.find('*').each((i, e) => { + let $e = $(e), + textContent = $e.text(); + if (textContent) { + isRTL(textContent) ? $e.attr('dir', 'rtl') : $e.attr('dir', 'ltr'); + } + }); +} + +export function siteDir() { + if (!_siteDir) { + _siteDir = $('html').hasClass('rtl') ? 'rtl' : 'ltr'; + } + return _siteDir; +} diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index 2fd90c59e01..fa8259b7df4 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -2,7 +2,8 @@ import parseHTML from 'discourse/helpers/parse-html'; const trimLeft = text => text.replace(/^\s+/,""); const trimRight = text => text.replace(/\s+$/,""); -const countPipes = text => text.replace(/\\\|/,"").match(/\|/g).length; +const countPipes = text => (text.replace(/\\\|/,"").match(/\|/g) || []).length; +const msoListClasses = ["MsoListParagraphCxSpFirst", "MsoListParagraphCxSpMiddle", "MsoListParagraphCxSpLast"]; class Tag { constructor(name, prefix = "", suffix = "", inline = false) { @@ -125,6 +126,14 @@ class Tag { decorate(text) { const attr = this.element.attributes; + if (/^mention/.test(attr.class) && "@" === text[0]) { + return text; + } + + if ("hashtag" === attr.class && "#" === text[0]) { + return text; + } + if (attr.href && text !== attr.href) { text = text.replace(/\n{2,}/g, "\n"); return "[" + text + "](" + attr.href + ")"; @@ -189,8 +198,14 @@ class Tag { toMarkdown() { const text = this.element.innerMarkdown().trim(); - if (text.includes("\n")) { - throw "Unsupported format inside Markdown table cells"; + if(text.includes("\n")) { // Unsupported format inside Markdown table cells + let e = this.element; + while(e = e.parent) { + if (e.name === "table") { + e.tag().invalid(); + break; + } + } } return this.decorate(text); @@ -201,7 +216,22 @@ class Tag { static li() { return class extends Tag.slice("li", "\n") { decorate(text) { - const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join(""); + let indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join(""); + const attrs = this.element.attributes; + + if (msoListClasses.includes(attrs.class)) { + try { + const level = parseInt(attrs.style.match(/level./)[0].replace("level", "")); + indent = Array(level).join("\t") + indent; + } finally { + if (attrs.class === "MsoListParagraphCxSpFirst") { + indent = `\n\n${indent}`; + } else if (attrs.class === "MsoListParagraphCxSpLast") { + text = `${text}\n`; + } + } + } + return super.decorate(`${indent}* ${trimLeft(text)}`); } }; @@ -242,21 +272,38 @@ class Tag { static table() { return class extends Tag.block("table") { + constructor() { + super(); + this.isValid = true; + } + + invalid() { + this.isValid = false; + if (this.element.parentNames.includes("table")) { + let e = this.element; + while(e = e.parent) { + if (e.name === "table") { + e.tag().invalid(); + break; + } + } + } + } + decorate(text) { text = super.decorate(text).replace(/\|\n{2,}\|/g, "|\n|"); const rows = text.trim().split("\n"); const pipeCount = countPipes(rows[0]); - const isValid = rows.length > 1 && - pipeCount > 2 && - rows.reduce((a, c) => a && countPipes(c) <= pipeCount); + this.isValid = this.isValid && rows.length > 1 && pipeCount > 2 && rows.reduce((a, c) => a && countPipes(c) <= pipeCount); // Unsupported table format for Markdown conversion - if (!isValid) { - throw "Unsupported table format for Markdown conversion"; + if (this.isValid) { + const splitterRow = [...Array(pipeCount-1)].map(() => "| --- ").join("") + "|\n"; + text = text.replace("|\n", "|\n" + splitterRow); + } else { + text = text.replace(/\|/g, " "); + this.invalid(); } - const splitterRow = [...Array(pipeCount-1)].map(() => "| --- ").join("") + "|\n"; - text = text.replace("|\n", "|\n" + splitterRow); - return text; } }; @@ -312,7 +359,7 @@ const tags = [ ...Tag.emphases().map((e) => Tag.emphasis(e[0], e[1])), Tag.cell("td"), Tag.cell("th"), Tag.replace("br", "\n"), Tag.replace("hr", "\n---\n"), Tag.replace("head", ""), - Tag.keep("ins"), Tag.keep("del"), Tag.keep("small"), Tag.keep("big"), + Tag.keep("ins"), Tag.keep("del"), Tag.keep("small"), Tag.keep("big"), Tag.keep("kbd"), Tag.li(), Tag.link(), Tag.image(), Tag.code(), Tag.blockquote(), Tag.table(), Tag.tr(), Tag.ol(), Tag.list("ul"), ]; @@ -333,6 +380,13 @@ class Element { this.parentNames = this.parentNames || []; this.previous = previous; this.next = next; + + if (this.name === "p") { + if (msoListClasses.includes(this.attributes.class)) { + this.name = "li"; + this.parentNames.push("ul"); + } + } } tag() { @@ -410,7 +464,7 @@ class Element { } } -function trimUnwantedSpaces(html) { +function trimUnwanted(html) { const body = html.match(/]*>([\s\S]*?)<\/body>/); html = body ? body[1] : html; html = html.replace(/\r|\n| /g, " "); @@ -420,6 +474,8 @@ function trimUnwantedSpaces(html) { html = html.replace(match[0], match[0].replace(/>\s{2,} <")); } + html = html.replace(/[^!]*/g, ""); // to support ms word list tags + return html; } @@ -438,7 +494,7 @@ function putPlaceholders(html) { match = codeRegEx.exec(origHtml); } - const elements = parseHTML(trimUnwantedSpaces(html)); + const elements = parseHTML(trimUnwanted(html)); return { elements, placeholders }; } diff --git a/app/assets/javascripts/discourse/lib/tooltip.js.es6 b/app/assets/javascripts/discourse/lib/tooltip.js.es6 new file mode 100644 index 00000000000..f1df4b3c9e6 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/tooltip.js.es6 @@ -0,0 +1,73 @@ +export function showTooltip() { + const fadeSpeed = 300; + const tooltipID = "#discourse-tooltip"; + const $this = $(this); + const $parent = $this.offsetParent(); + const content = $this.attr("data-tooltip"); + const retina = window.devicePixelRatio && window.devicePixelRatio > 1 ? "class='retina'" : ""; + + let pos = $this.offset(); + const delta = $parent.offset(); + pos.top -= delta.top; + pos.left -= delta.left; + + $(tooltipID).fadeOut(fadeSpeed).remove(); + + $(this).after(` +
      +
      +
      ${content}
      +
      + `); + + $(window).on("click.discourse", (event) => { + if ($(event.target).closest(tooltipID).length === 0) { + $(tooltipID).remove(); + $(window).off("click.discourse"); + } + return true; + }); + + const $tooltip = $(tooltipID); + $tooltip.css({top: 0, left: 0}); + + let left = (pos.left - ($tooltip.width() / 2) + $this.width()/2); + if (left < 0) { + $tooltip.find(".tooltip-pointer").css({ + "margin-left": left*2 + "px" + }); + left = 0; + } + + // also do a right margin fix + const parentWidth = $parent.width(); + if (left + $tooltip.width() > parentWidth) { + let oldLeft = left; + left = parentWidth - $tooltip.width(); + + $tooltip.find(".tooltip-pointer").css({ + "margin-left": (oldLeft - left) * 2 + "px" + }); + } + + $tooltip.css({ + top: pos.top + 5 + "px", + left: left + "px" + }); + + $tooltip.fadeIn(fadeSpeed); + + return false; +} + +export function registerTooltip(jqueryContext) { + if (jqueryContext.length) { + jqueryContext.on('click', showTooltip); + } +} + +export function unregisterTooltip(jqueryContext) { + if (jqueryContext.length) { + jqueryContext.off('click'); + } +} diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index ac81e64e682..e205dea1d02 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -27,7 +27,7 @@ export function transformBasicPost(post) { deleted: post.get('deleted'), deleted_at: post.deleted_at, user_deleted: post.user_deleted, - isDeleted: post.deleted_at || post.user_deleted, + isDeleted: post.deleted_at || post.user_deleted, // xxxxx deletedByAvatarTemplate: null, deletedByUsername: null, primary_group_name: post.primary_group_name, @@ -77,6 +77,7 @@ export function transformBasicPost(post) { cooked_hidden: !!post.cooked_hidden, expandablePost: false, replyCount: post.reply_count, + locked: post.locked }; _additionalAttributes.forEach(a => postAtts[a] = post[a]); @@ -137,10 +138,12 @@ export default function transformPost(currentUser, site, post, prevPost, nextPos postAtts.topicCreatedAt = topic.created_at; postAtts.createdByUsername = createdBy.username; postAtts.createdByAvatarTemplate = createdBy.avatar_template; + postAtts.createdByName = createdBy.name; postAtts.lastPostUrl = topic.get('lastPostUrl'); postAtts.lastPostUsername = details.last_poster.username; postAtts.lastPostAvatarTemplate = details.last_poster.avatar_template; + postAtts.lastPostName = details.last_poster.name; postAtts.lastPostAt = topic.last_posted_at; postAtts.topicReplyCount = topic.get('replyCount'); @@ -212,7 +215,8 @@ export default function transformPost(currentUser, site, post, prevPost, nextPos postAtts.expandablePost = topic.expandable_first_post; } else { postAtts.canRecover = postAtts.isDeleted && postAtts.canRecover; - postAtts.canDelete = !postAtts.isDeleted && postAtts.canDelete; + postAtts.canDelete = postAtts.canDelete && !post.deleted_at && + currentUser && (currentUser.staff || !post.user_deleted); } _additionalAttributes.forEach(a => postAtts[a] = post[a]); diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 2fe1b79a8bc..16acbbf2655 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -5,6 +5,11 @@ import { defaultHomepage } from 'discourse/lib/utilities'; const rewrites = []; const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; +function redirectTo(url) { + document.location = url; + return true; +} + // We can add links here that have server side responses but not client side. const SERVER_SIDE_ONLY = [ /^\/assets\//, @@ -17,6 +22,7 @@ const SERVER_SIDE_ONLY = [ /^\/wizard/, /\.rss$/, /\.json$/, + /^\/admin\/upgrade$/ ]; export function rewritePath(path) { @@ -75,6 +81,7 @@ const DiscourseURL = Ember.Object.extend({ const holderId = `#post_${postNumber}`; _transitioning = postNumber > 1; + Ember.run.schedule('afterRender', () => { let elementId; let holder; @@ -101,7 +108,7 @@ const DiscourseURL = Ember.Object.extend({ } }); - if (holder.length > 0 && opts && opts.skipIfOnScreen){ + if (holder.length > 0 && opts && opts.skipIfOnScreen) { const elementTop = lockon.elementTop(); const scrollTop = $(window).scrollTop(); const windowHeight = $(window).height() - offsetCalculator(); @@ -162,15 +169,23 @@ const DiscourseURL = Ember.Object.extend({ if (Em.isEmpty(path)) { return; } if (Discourse.get('requiresRefresh')) { - document.location.href = Discourse.getURL(path); - return; + return redirectTo(Discourse.getURL(path)); } const pathname = path.replace(/(https?\:)?\/\/[^\/]+/, ''); + const baseUri = Discourse.BaseUri; + + // If we have a baseUri and an absolute URL, make sure the baseUri + // is the same. Otherwise we could be switching forums. + if (baseUri && + path.indexOf('http') === 0 && + pathname.indexOf(baseUri) !== 0) { + return redirectTo(path); + } + const serverSide = SERVER_SIDE_ONLY.some(r => { if (pathname.match(r)) { - document.location = path; - return true; + return redirectTo(path); } }); @@ -178,8 +193,7 @@ const DiscourseURL = Ember.Object.extend({ // Protocol relative URLs if (path.indexOf('//') === 0) { - document.location = path; - return; + return redirectTo(path); } // Scroll to the same page, different anchor @@ -193,29 +207,29 @@ const DiscourseURL = Ember.Object.extend({ path = path.replace(/(https?\:)?\/\/[^\/]+/, ''); // Rewrite /my/* urls - if (path.indexOf(Discourse.BaseUri + '/my/') === 0) { + let myPath = `${baseUri}/my/`; + if (path.indexOf(myPath) === 0) { const currentUser = Discourse.User.current(); if (currentUser) { - path = path.replace(Discourse.BaseUri + '/my/', userPath(currentUser.get('username_lower') + "/")); + path = path.replace(myPath, userPath(currentUser.get('username_lower') + "/")); } else { - document.location.href = "/404"; - return; + return redirectTo('/404'); } } // handle prefixes - if (path.match(/^\//)) { - let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri); - rootURL = rootURL.replace(/\/$/, ''); - path = path.replace(rootURL, ''); + if (path.indexOf("/") === 0) { + const rootURL = (baseUri === undefined ? "/" : baseUri).replace(/\/$/, ""); + path = path.replace(rootURL, ""); } path = rewritePath(path); + if (this.navigatedToPost(oldPath, path, opts)) { return; } if (oldPath === path) { - // If navigating to the same path send an app event. Views can watch it - // and tell their controllers to refresh + // If navigating to the same path send an app event. + // Views can watch it and tell their controllers to refresh this.appEvents.trigger('url:refresh'); } diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 29ad228a050..84edc4e4d62 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -1,5 +1,6 @@ import { CANCELLED_STATUS } from 'discourse/lib/autocomplete'; import { userPath } from 'discourse/lib/url'; +import { emailValid } from 'discourse/lib/utilities'; var cache = {}, cacheTopicId, @@ -61,7 +62,7 @@ function organizeResults(r, options) { }); } - if (options.term.match(/@/)) { + if (!options.disallowEmails && emailValid(options.term)) { let e = { username: options.term }; emails = [ e ]; results.push(e); @@ -69,10 +70,11 @@ function organizeResults(r, options) { if (r.groups) { r.groups.every(function(g) { - if (results.length > limit && options.term.toLowerCase() !== g.name.toLowerCase()) return false; - if (exclude.indexOf(g.name) === -1) { - groups.push(g); - results.push(g); + if (options.term.toLowerCase() === g.name.toLowerCase() || results.length < limit) { + if (exclude.indexOf(g.name) === -1) { + groups.push(g); + results.push(g); + } } return true; }); diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 33558befacb..91a48e0de6e 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -1,4 +1,5 @@ import { escape } from 'pretty-text/sanitizer'; +import toMarkdown from 'discourse/lib/to-markdown'; const homepageSelector = 'meta[name=discourse_current_homepage]'; @@ -113,12 +114,8 @@ export function selectedText() { $div.find(".clicks").remove(); // replace emojis $div.find("img.emoji").replaceWith(function() { return this.title; }); - // replace br with newlines - $div.find("br").replaceWith(() => "\n"); - // enforce newline at the end of paragraphs - $div.find("p").append(() => "\n"); - return String($div.text()).trim().replace(/(^\s*\n)+/gm, "\n"); + return toMarkdown($div.html()); } // Determine the row and col of the caret in an element @@ -193,6 +190,8 @@ export function validateUploadedFiles(files, opts) { } export function validateUploadedFile(file, opts) { + if (!authorizesOneOrMoreExtensions()) return false; + opts = opts || {}; const name = file && file.name; @@ -201,7 +200,7 @@ export function validateUploadedFile(file, opts) { // check that the uploaded file is authorized if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) { - if (Discourse.User.current("staff")) { + if (Discourse.User.currentProp('staff')) { return true; } } @@ -237,16 +236,28 @@ export function validateUploadedFile(file, opts) { const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i; +function extensionsToArray(exts) { + return exts.toLowerCase() + .replace(/[\s\.]+/g, "") + .split("|") + .filter(ext => ext.indexOf("*") === -1); +} + function extensions() { - return Discourse.SiteSettings.authorized_extensions - .toLowerCase() - .replace(/[\s\.]+/g, "") - .split("|") - .filter(ext => ext.indexOf("*") === -1); + return extensionsToArray(Discourse.SiteSettings.authorized_extensions); +} + +function staffExtensions() { + return extensionsToArray(Discourse.SiteSettings.authorized_extensions_for_staff); } function imagesExtensions() { - return extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + if (Discourse.User.currentProp('staff')) { + const staffExts = staffExtensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + exts = _.union(exts, staffExts); + } + return exts; } function extensionsRegex() { @@ -257,7 +268,14 @@ function imagesExtensionsRegex() { return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i"); } +function staffExtensionsRegex() { + return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i"); +} + function isAuthorizedFile(fileName) { + if (Discourse.User.currentProp('staff') && staffExtensionsRegex().test(fileName)) { + return true; + } return extensionsRegex().test(fileName); } @@ -266,7 +284,8 @@ function isAuthorizedImage(fileName){ } export function authorizedExtensions() { - return authorizesAllExtensions() ? "*" : extensions().join(", "); + const exts = Discourse.User.currentProp('staff') ? [...extensions(), ...staffExtensions()] : extensions(); + return exts.filter(ext => ext.length > 0).join(", "); } export function authorizedImagesExtensions() { @@ -274,7 +293,24 @@ export function authorizedImagesExtensions() { } export function authorizesAllExtensions() { - return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0; + return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 || ( + Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 && + Discourse.User.currentProp('staff')); +} + +export function authorizesOneOrMoreExtensions() { + if (authorizesAllExtensions()) return true; + + return Discourse.SiteSettings.authorized_extensions + .split("|") + .filter(ext => ext) + .length > 0; +} + +export function authorizesOneOrMoreImageExtensions() { + if (authorizesAllExtensions()) return true; + + return imagesExtensions().length > 0; } export function isAnImage(path) { @@ -291,13 +327,13 @@ function isGUID(value) { function imageNameFromFileName(fileName) { const split = fileName.split('.'); - const name = split[split.length-2]; + let name = split[split.length - 2]; if (exports.isAppleDevice() && isGUID(name)) { - return I18n.t('upload_selector.default_image_alt_text'); + name = I18n.t('upload_selector.default_image_alt_text'); } - return name; + return encodeURIComponent(name); } export function allowsImages() { @@ -305,7 +341,7 @@ export function allowsImages() { } export function allowsAttachments() { - return authorizesAllExtensions() || extensions().length > imagesExtensions().length; + return authorizesAllExtensions() || authorizedExtensions().split(", ").length > imagesExtensions().length; } export function uploadLocation(url) { @@ -352,7 +388,7 @@ export function displayErrorForUpload(data) { if (data.jqXHR.responseJSON.message) { bootbox.alert(data.jqXHR.responseJSON.message); } else { - bootbox.alert(data.jqXHR.responseJSON.join("\n")); + bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")); } return; } @@ -450,5 +486,25 @@ export function clipboardData(e, canUpload) { return { clipboard, types, canUpload, canPasteHtml }; } +export function fillMissingDates(data, startDate, endDate) { + const startMoment = moment(startDate, "YYYY-MM-DD"); + const endMoment = moment(endDate, "YYYY-MM-DD"); + const countDays = endMoment.diff(startMoment, 'days'); + let currentMoment = startMoment; + + for (let i = 0; i <= countDays; i++) { + let date = (data[i]) ? moment(data[i].x, "YYYY-MM-DD") : null; + if (i === 0 && (!date || date.isAfter(startMoment))) { + data.splice(i, 0, { "x" : startMoment.format("YYYY-MM-DD"), 'y': 0 }); + } else { + if (!date || date.isAfter(moment(currentMoment))) { + data.splice(i, 0, { "x" : currentMoment, 'y': 0 }); + } + } + currentMoment = moment(currentMoment).add(1, "day").format("YYYY-MM-DD"); + } + return data; +} + // This prevents a mini racer crash export default {}; diff --git a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 index dc802c4d047..8e15c955c26 100644 --- a/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 +++ b/app/assets/javascripts/discourse/mixins/can-check-emails.js.es6 @@ -1,9 +1,9 @@ import { propertyEqual, setting } from 'discourse/lib/computed'; export default Ember.Mixin.create({ - isOwnEmail: propertyEqual("model.id", "currentUser.id"), + isCurrentUser: propertyEqual("model.id", "currentUser.id"), showEmailOnProfile: setting("show_email_on_profile"), canStaffCheckEmails: Em.computed.and("showEmailOnProfile", "currentUser.staff"), canAdminCheckEmails: Em.computed.alias("currentUser.admin"), - canCheckEmails: Em.computed.or("isOwnEmail", "canStaffCheckEmails", "canAdminCheckEmails"), + canCheckEmails: Em.computed.or("isCurrentUser", "canStaffCheckEmails", "canAdminCheckEmails"), }); diff --git a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 new file mode 100644 index 00000000000..0ff1193368a --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 @@ -0,0 +1,198 @@ +import { wantsNewWindow } from 'discourse/lib/intercept-click'; +import afterTransition from 'discourse/lib/after-transition'; +import DiscourseURL from 'discourse/lib/url'; + +export default Ember.Mixin.create({ + elementId: null, //click detection added for data-{elementId} + triggeringLinkClass: null, //the classname where this card should appear + _showCallback: null, //username, $target - load up data for when show is called, should call this._positionCard($target) when it's done. + + postStream: Ember.computed.alias('topic.postStream'), + viewingTopic: Ember.computed.match('currentPath', /^topic\./), + + visible: false, + username: null, + loading: null, + cardTarget: null, + post: null, + isFixed: false, + + _show(username, $target) { + // No user card for anon + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { + return false; + } + + username = Ember.Handlebars.Utils.escapeExpression(username.toString()); + + // Don't show on mobile + if (this.site.mobileView) { + DiscourseURL.routeTo($target.attr("href")); + return false; + } + + const currentUsername = this.get('username'); + if (username === currentUsername && this.get('loading') === username) { + return; + } + + const postId = $target.parents('article').data('post-id'); + const wasVisible = this.get('visible'); + const previousTarget = this.get('cardTarget'); + const target = $target[0]; + + if (wasVisible) { + this._close(); + if (target === previousTarget) { return; } + } + + const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null; + this.setProperties({ username, loading: username, cardTarget: target, post }); + + this._showCallback(username, $target); + + return false; + }, + + didInsertElement() { + this._super(); + afterTransition(this.$(), this._hide.bind(this)); + const id = this.get('elementId'); + const triggeringLinkClass = this.get('triggeringLinkClass'); + const clickOutsideEventName = `mousedown.outside-${id}`; + const clickDataExpand = `click.discourse-${id}`; + const clickMention = `click.discourse-${id}-${triggeringLinkClass}`; + const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`; + + this.setProperties({ clickOutsideEventName, clickDataExpand, clickMention, previewClickEvent }); + + $('html').off(clickOutsideEventName) + .on(clickOutsideEventName, (e) => { + if (this.get('visible')) { + const $target = $(e.target); + if ($target.closest(`[data-${id}]`).data(id) || + $target.closest(`a.${triggeringLinkClass}`).length > 0 || + $target.closest(`#${id}`).length > 0) { + return; + } + + this._close(); + } + + return true; + }); + + $('#main-outlet').on(clickDataExpand, `[data-${id}]`, (e) => { + if (wantsNewWindow(e)) { return; } + const $target = $(e.currentTarget); + return this._show($target.data(id), $target); + }); + + $('#main-outlet').on(clickMention, `a.${triggeringLinkClass}`, (e) => { + if (wantsNewWindow(e)) { return; } + const $target = $(e.target); + return this._show($target.text().replace(/^@/, ''), $target); + }); + + this.appEvents.on(previewClickEvent, $target => { + this.set('isFixed', true); + return this._show($target.text().replace(/^@/, ''), $target); + }); + }, + + _positionCard(target) { + const rtl = ($('html').css('direction')) === 'rtl'; + if (!target) { return; } + const width = this.$().width(); + const height = 175; + const isFixed = this.get('isFixed'); + + let verticalAdjustments = 0; + + Ember.run.schedule('afterRender', () => { + if (target) { + let position = target.offset(); + if (position) { + position.bottom = 'unset'; + + if (rtl) { // The site direction is rtl + position.right = $(window).width() - position.left + 10; + position.left = 'auto'; + let overage = ($(window).width() - 50) - (position.right + width); + if (overage < 0) { + position.right += overage; + position.top += target.height() + 48; + verticalAdjustments += target.height() + 48; + } + } else { // The site direction is ltr + position.left += target.width() + 10; + + let overage = ($(window).width() - 50) - (position.left + width); + if (overage < 0) { + position.left += overage; + position.top += target.height() + 48; + verticalAdjustments += target.height() + 48; + } + } + + position.top -= $('#main-outlet').offset().top; + if(isFixed) { + position.top -= $('html').scrollTop(); + //if content is fixed and will be cut off on the bottom, display it above... + if(position.top + height + verticalAdjustments > $(window).height() - 50) { + position.bottom = $(window).height() - (target.offset().top - $('html').scrollTop()); + if(verticalAdjustments > 0) { + position.bottom += 48; + } + position.top = 'unset'; + } + } + this.$().css(position); + } + + // After the card is shown, focus on the first link + // + // note: we DO NOT use afterRender here cause _positionCard may + // run afterwards, if we allowed this to happen the usercard + // may be offscreen and we may scroll all the way to it on focus + Ember.run.next(null, () => this.$('a:first').focus() ); + } + }); + }, + + _hide() { + if (!this.get('visible')) { + this.$().css({left: -9999, top: -9999}); + } + }, + + _close() { + this.setProperties({ + visible: false, + username: null, + loading: null, + cardTarget: null, + post: null, + isFixed: false + }); + }, + + willDestroyElement() { + this._super(); + const clickOutsideEventName = this.get('clickOutsideEventName'); + const clickDataExpand = this.get('clickDataExpand'); + const clickMention = this.get('clickMention'); + const previewClickEvent = this.get('previewClickEvent'); + $('html').off(clickOutsideEventName); + $('#main').off(clickDataExpand).off(clickMention); + this.appEvents.off(previewClickEvent); + }, + + keyUp(e) { + if (e.keyCode === 27) { // ESC + const target = this.get('cardTarget'); + this._close(); + target.focus(); + } + } +}); diff --git a/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6 b/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6 new file mode 100644 index 00000000000..6000f041917 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/grant-badge-controller.js.es6 @@ -0,0 +1,38 @@ +import computed from "ember-addons/ember-computed-decorators"; +import UserBadge from 'discourse/models/user-badge'; + +export default Ember.Mixin.create({ + @computed('allBadges.[]', 'userBadges.[]') + grantableBadges(allBadges, userBadges) { + const granted = userBadges.reduce((map, badge) => { + map[badge.get('badge_id')] = true; + return map; + }, {}); + + return allBadges + .filter(badge => { + return badge.get('enabled') + && badge.get('manually_grantable') + && (!granted[badge.get('id')] || badge.get('multiple_grant')); + }) + .sort((a, b) => a.get('name').localeCompare(b.get('name'))); + }, + + noGrantableBadges: Ember.computed.empty('grantableBadges'), + + @computed('selectedBadgeId', 'grantableBadges') + selectedBadgeGrantable(selectedBadgeId, grantableBadges) { + return grantableBadges && grantableBadges.find(badge => badge.get('id') === selectedBadgeId); + }, + + grantBadge(selectedBadgeId, username, badgeReason) { + return UserBadge.grant(selectedBadgeId, username, badgeReason) + .then(newBadge => { + this.get('userBadges').pushObject(newBadge); + return newBadge; + }, error => { + throw error; + }); + } +}); + diff --git a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 index d78478dae63..b34b906bdcd 100644 --- a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 +++ b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 @@ -5,6 +5,10 @@ export default Ember.Mixin.create({ this.appEvents.trigger('modal-body:flash', { text, messageClass }); }, + clearFlash() { + this.appEvents.trigger('modal-body:clearFlash'); + }, + showModal(...args) { return showModal(...args); }, diff --git a/app/assets/javascripts/discourse/mixins/open-composer.js.es6 b/app/assets/javascripts/discourse/mixins/open-composer.js.es6 index 2303281b63b..dcb92d14786 100644 --- a/app/assets/javascripts/discourse/mixins/open-composer.js.es6 +++ b/app/assets/javascripts/discourse/mixins/open-composer.js.es6 @@ -7,8 +7,8 @@ export default Ember.Mixin.create({ this.controllerFor('composer').open({ categoryId: controller.get('category.id'), action: Composer.CREATE_TOPIC, - draftKey: controller.get('model.draft_key'), - draftSequence: controller.get('model.draft_sequence') + draftKey: controller.get('model.draft_key') || Composer.CREATE_TOPIC, + draftSequence: controller.get('model.draft_sequence') || 0 }); }, diff --git a/app/assets/javascripts/discourse/models/category-list.js.es6 b/app/assets/javascripts/discourse/models/category-list.js.es6 index 987e5020530..9511c49c589 100644 --- a/app/assets/javascripts/discourse/models/category-list.js.es6 +++ b/app/assets/javascripts/discourse/models/category-list.js.es6 @@ -37,7 +37,6 @@ CategoryList.reopenClass({ c.topics = c.topics.map(t => Discourse.Topic.create(t)); } - switch(statPeriod) { case "week": case "month": diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index 52c3eb443c7..e30a2ba7dd2 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -97,7 +97,7 @@ const Category = RestModel.extend({ allow_badges: this.get('allow_badges'), custom_fields: this.get('custom_fields'), topic_template: this.get('topic_template'), - suppress_from_homepage: this.get('suppress_from_homepage'), + suppress_from_latest: this.get('suppress_from_latest'), all_topics_wiki: this.get('all_topics_wiki'), allowed_tags: this.get('allowed_tags'), allowed_tag_groups: this.get('allowed_tag_groups'), @@ -108,7 +108,8 @@ const Category = RestModel.extend({ num_featured_topics: this.get('num_featured_topics'), default_view: this.get('default_view'), subcategory_list_style: this.get('subcategory_list_style'), - default_top_period: this.get('default_top_period') + default_top_period: this.get('default_top_period'), + minimum_required_tags: this.get('minimum_required_tags') }, type: id ? 'PUT' : 'POST' }); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 4cdc2bd4c43..cefd6df7ddf 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -1,4 +1,3 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; import RestModel from 'discourse/models/rest'; import Topic from 'discourse/models/topic'; import { throwAjaxError } from 'discourse/lib/ajax-error'; @@ -6,51 +5,70 @@ import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; import computed from 'ember-addons/ember-computed-decorators'; import { escapeExpression, tinyAvatar } from 'discourse/lib/utilities'; -import { emojiUnescape } from 'discourse/lib/text'; + +// The actions the composer can take +export const + CREATE_TOPIC = 'createTopic', + CREATE_SHARED_DRAFT = 'createSharedDraft', + EDIT_SHARED_DRAFT = 'editSharedDraft', + PRIVATE_MESSAGE = 'privateMessage', + NEW_PRIVATE_MESSAGE_KEY = 'new_private_message', + REPLY = 'reply', + EDIT = 'edit', + REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic", + REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message"; + +function isEdit(action) { + return action === EDIT || action === EDIT_SHARED_DRAFT; +} const CLOSED = 'closed', - SAVING = 'saving', - OPEN = 'open', - DRAFT = 'draft', + SAVING = 'saving', + OPEN = 'open', + DRAFT = 'draft', - // The actions the composer can take - CREATE_TOPIC = 'createTopic', - PRIVATE_MESSAGE = 'privateMessage', - NEW_PRIVATE_MESSAGE_KEY = 'new_private_message', - REPLY = 'reply', - EDIT = 'edit', - REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic", - REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message", + // When creating, these fields are moved into the post model from the composer model + _create_serializer = { + raw: 'reply', + title: 'title', + unlist_topic: 'unlistTopic', + category: 'categoryId', + topic_id: 'topic.id', + is_warning: 'isWarning', + whisper: 'whisper', + archetype: 'archetypeId', + target_usernames: 'targetUsernames', + typing_duration_msecs: 'typingTime', + composer_open_duration_msecs: 'composerTime', + tags: 'tags', + featured_link: 'featuredLink', + shared_draft: 'sharedDraft' + }, - // When creating, these fields are moved into the post model from the composer model - _create_serializer = { - raw: 'reply', - title: 'title', - unlist_topic: 'unlistTopic', - category: 'categoryId', - topic_id: 'topic.id', - is_warning: 'isWarning', - whisper: 'whisper', - archetype: 'archetypeId', - target_usernames: 'targetUsernames', - typing_duration_msecs: 'typingTime', - composer_open_duration_msecs: 'composerTime', - tags: 'tags', - featured_link: 'featuredLink' - }, + _edit_topic_serializer = { + title: 'topic.title', + categoryId: 'topic.category.id', + tags: 'topic.tags', + featuredLink: 'topic.featured_link' + }; - _edit_topic_serializer = { - title: 'topic.title', - categoryId: 'topic.category.id', - tags: 'topic.tags', - featuredLink: 'topic.featured_link' - }; +const SAVE_LABELS = { + [EDIT]: 'composer.save_edit', + [REPLY]: 'composer.reply', + [CREATE_TOPIC]: 'composer.create_topic', + [PRIVATE_MESSAGE]: 'composer.create_pm', + [CREATE_SHARED_DRAFT]: 'composer.create_shared_draft', + [EDIT_SHARED_DRAFT]: 'composer.save_edit' +}; -const _saveLabels = {}; -_saveLabels[EDIT] = 'composer.save_edit'; -_saveLabels[REPLY] = 'composer.reply'; -_saveLabels[CREATE_TOPIC] = 'composer.create_topic'; -_saveLabels[PRIVATE_MESSAGE] = 'composer.create_pm'; +const SAVE_ICONS = { + [EDIT]: 'pencil', + [EDIT_SHARED_DRAFT]: 'clipboard', + [REPLY]: 'reply', + [CREATE_TOPIC]: 'plus', + [PRIVATE_MESSAGE]: 'envelope', + [CREATE_SHARED_DRAFT]: 'clipboard' +}; const Composer = RestModel.extend({ _categoryId: null, @@ -60,6 +78,8 @@ const Composer = RestModel.extend({ return this.site.get('archetypes'); }.property(), + @computed('action') + sharedDraft: action => action === CREATE_SHARED_DRAFT, @computed categoryId: { @@ -85,7 +105,13 @@ const Composer = RestModel.extend({ return categoryId ? this.site.categories.findBy('id', categoryId) : null; }, + @computed('category') + minimumRequiredTags(category) { + return (category && category.get('minimum_required_tags') > 0) ? category.get('minimum_required_tags') : null; + }, + creatingTopic: Em.computed.equal('action', CREATE_TOPIC), + creatingSharedDraft: Em.computed.equal('action', CREATE_SHARED_DRAFT), creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE), notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'), @@ -102,13 +128,14 @@ const Composer = RestModel.extend({ topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'), - editingPost: Em.computed.equal('action', EDIT), + @computed('action') + editingPost: isEdit, + replyingToTopic: Em.computed.equal('action', REPLY), viewOpen: Em.computed.equal('composeState', OPEN), viewDraft: Em.computed.equal('composeState', DRAFT), - composeStateChanged: function() { var oldOpen = this.get('composerOpened'); @@ -149,7 +176,14 @@ const Composer = RestModel.extend({ }, 100, {leading: false, trailing: true}), editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'), - canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'), + + canEditTitle: Em.computed.or( + 'creatingTopic', + 'creatingPrivateMessage', + 'editingFirstPost', + 'creatingSharedDraft' + ), + canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'), @computed('canEditTitle', 'creatingPrivateMessage', 'categoryId') @@ -167,78 +201,98 @@ const Composer = RestModel.extend({ return this.get('canEditTopicFeaturedLink') ? 'composer.title_or_link_placeholder' : 'composer.title_placeholder'; }, - // Determine the appropriate title for this action - actionTitle: function() { - const topic = this.get('topic'); + @computed("action", "post", "topic", "topic.title") + replyOptions(action, post, topic, topicTitle) { + let options = { + userLink: null, + topicLink: null, + postLink: null, + userAvatar: null, + originalUser: null + }; - let postLink, topicLink, usernameLink; if (topic) { - const postNumber = this.get('post.post_number'); - postLink = "" + - I18n.t("post.post_number", { number: postNumber }) + ""; - - let title = topic.get('fancy_title') || escapeExpression(topic.get('title')); - - topicLink = " " + title + ""; - usernameLink = "" + this.get('post.username') + ""; + options.topicLink = { + href: topic.get("url"), + anchor: topic.get("fancy_title") || escapeExpression(topicTitle) + }; } - let postDescription; - const post = this.get('post'); - if (post) { - postDescription = I18n.t('post.' + this.get('action'), { - link: postLink, - replyAvatar: tinyAvatar(post.get('avatar_template')), - username: this.get('post.username'), - usernameLink - }); + options.label = I18n.t(`post.${action}`); + options.userAvatar = tinyAvatar(post.get("avatar_template")); if (!this.site.mobileView) { - const replyUsername = post.get('reply_to_user.username'); - const replyAvatarTemplate = post.get('reply_to_user.avatar_template'); - if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) { - postDescription += ` ${iconHTML('mail-forward', { class: 'reply-to-glyph' })} ` + tinyAvatar(replyAvatarTemplate) + " " + replyUsername; + const originalUserName = post.get('reply_to_user.username'); + const originalUserAvatar = post.get('reply_to_user.avatar_template'); + if (originalUserName && originalUserAvatar && isEdit(action)) { + options.originalUser = { + username: originalUserName, + avatar: tinyAvatar(originalUserAvatar) + }; } } } - switch (this.get('action')) { - case PRIVATE_MESSAGE: return I18n.t('topic.private_message'); - case CREATE_TOPIC: return I18n.t('topic.create_long'); - case REPLY: - case EDIT: - if (postDescription) return postDescription; - if (topic) return emojiUnescape(I18n.t('post.reply_topic', { link: topicLink })); + if (topic && post) { + const postNumber = post.get("post_number"); + + options.postLink = { + href: `${topic.get("url")}/${postNumber}`, + anchor: I18n.t("post.post_number", { number: postNumber }) + }; + + options.userLink = { + href: `${topic.get("url")}/${postNumber}`, + anchor: post.get("username") + }; } - }.property('action', 'post', 'topic', 'topic.title'), + return options; + }, - // whether to disable the post button - cantSubmitPost: function() { + @computed + isStaffUser() { + const currentUser = Discourse.User.current(); + return currentUser && currentUser.get('staff'); + }, + + @computed('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters', 'tags', 'topicFirstPost', 'minimumRequiredTags', 'isStaffUser') + cantSubmitPost(loading, canEditTitle, titleLength, targetUsernames, replyLength, categoryId, missingReplyCharacters, tags, topicFirstPost, minimumRequiredTags, isStaffUser) { // can't submit while loading - if (this.get('loading')) return true; + if (loading) return true; // title is required when // - creating a new topic/private message // - editing the 1st post - if (this.get('canEditTitle') && !this.get('titleLengthValid')) return true; + if (canEditTitle && !this.get('titleLengthValid')) return true; // reply is always required - if (this.get('missingReplyCharacters') > 0) return true; + if (missingReplyCharacters > 0) return true; + + if (this.site.get('can_tag_topics') && !isStaffUser && topicFirstPost && minimumRequiredTags) { + const tagsArray = tags || []; + if (tagsArray.length < minimumRequiredTags) { + return true; + } + } if (this.get("privateMessage")) { // need at least one user when sending a PM - return this.get('targetUsernames') && (this.get('targetUsernames').trim() + ',').indexOf(',') === 0; + return targetUsernames && (targetUsernames.trim() + ',').indexOf(',') === 0; } else { // has a category? (when needed) - return this.get('canCategorize') && - !this.siteSettings.allow_uncategorized_topics && - !this.get('categoryId') && - !this.user.get('admin'); + return this.get('requiredCategoryMissing'); } - }.property('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters'), + }, + + @computed('canCategorize', 'categoryId') + requiredCategoryMissing(canCategorize, categoryId) { + return canCategorize && !categoryId && + !this.siteSettings.allow_uncategorized_topics && + !this.user.get('admin'); + }, titleLengthValid: function() { if (this.user.get('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true; @@ -248,17 +302,12 @@ const Composer = RestModel.extend({ @computed('action') saveIcon(action) { - switch (action) { - case EDIT: return 'pencil'; - case REPLY: return 'reply'; - case CREATE_TOPIC: 'plus'; - case PRIVATE_MESSAGE: 'envelope'; - } + return SAVE_ICONS[action]; }, @computed('action', 'whisper') saveLabel(action, whisper) { - return whisper ? 'composer.create_whisper' : _saveLabels[action]; + return whisper ? 'composer.create_whisper' : SAVE_LABELS[action]; }, hasMetaData: function() { @@ -302,7 +351,7 @@ const Composer = RestModel.extend({ @computed('privateMessage') minimumTitleLength(privateMessage) { if (privateMessage) { - return this.siteSettings.min_private_message_title_length; + return this.siteSettings.min_personal_message_title_length; } else { return this.siteSettings.min_topic_title_length; } @@ -325,7 +374,7 @@ const Composer = RestModel.extend({ if (pmWithNonHumanUser) { return 1; } else if (privateMessage) { - return this.siteSettings.min_private_message_post_length; + return this.siteSettings.min_personal_message_post_length; } else if (topicFirstPost) { // first post (topic body) return this.siteSettings.min_first_post_length; @@ -452,11 +501,11 @@ const Composer = RestModel.extend({ const composer = this; if (!replyBlank && - ((opts.reply || opts.action === EDIT) && this.get('replyDirty'))) { + ((opts.reply || isEdit(opts.action)) && this.get('replyDirty'))) { return; } - if (opts.action === REPLY && this.get('action') === EDIT) this.set('reply', ''); + if (opts.action === REPLY && isEdit(this.get('action'))) this.set('reply', ''); if (!opts.draftKey) throw 'draft key is required'; if (opts.draftSequence === null) throw 'draft sequence is required'; @@ -469,7 +518,8 @@ const Composer = RestModel.extend({ targetUsernames: opts.usernames, composerTotalOpened: opts.composerTime, typingTime: opts.typingTime, - whisper: opts.whisper + whisper: opts.whisper, + tags: opts.tags }); if (opts.post) { @@ -508,11 +558,15 @@ const Composer = RestModel.extend({ } // If we are editing a post, load it. - if (opts.action === EDIT && opts.post) { + if (isEdit(opts.action) && opts.post) { const topicProps = this.serialize(_edit_topic_serializer); topicProps.loading = true; + // When editing a shared draft, use its category + if (opts.action === EDIT_SHARED_DRAFT && opts.destinationCategoryId) { + topicProps.categoryId = opts.destinationCategoryId; + } this.setProperties(topicProps); this.store.find('post', opts.post.get('id')).then(function(post) { @@ -572,21 +626,25 @@ const Composer = RestModel.extend({ // When you edit a post editPost(opts) { - const post = this.get('post'), - oldCooked = post.get('cooked'), - self = this; + let post = this.get('post'); + let oldCooked = post.get('cooked'); + let promise = Ember.RSVP.resolve(); - let promise; - - // Update the title if we've changed it, otherwise consider it a - // successful resolved promise + // Update the topic if we're editing the first post if (this.get('title') && post.get('post_number') === 1 && this.get('topic.details.can_edit')) { const topicProps = this.getProperties(Object.keys(_edit_topic_serializer)); - promise = Topic.update(this.get('topic'), topicProps); - } else { - promise = Ember.RSVP.resolve(); + + let topic = this.get('topic'); + + // If we're editing a shared draft, keep the original category + if (this.get('action') === EDIT_SHARED_DRAFT) { + let destinationCategoryId = topicProps.categoryId; + promise = promise.then(() => topic.updateDestinationCategory(destinationCategoryId)); + topicProps.categoryId = topic.get('category.id'); + } + promise = promise.then(() => Topic.update(topic, topicProps)); } const props = { @@ -598,18 +656,18 @@ const Composer = RestModel.extend({ this.set('composeState', SAVING); - var rollback = throwAjaxError(function(){ + let rollback = throwAjaxError(() => { post.set('cooked', oldCooked); - self.set('composeState', OPEN); + this.set('composeState', OPEN); }); - return promise.then(function() { + return promise.then(() => { // rest model only sets props after it is saved post.set("cooked", props.cooked); - return post.save(props).then(function(result) { - self.clearState(); + return post.save(props).then(result => { + this.clearState(); return result; - }).catch(function(error) { + }).catch(error => { throw error; }); }).catch(rollback); @@ -779,7 +837,8 @@ const Composer = RestModel.extend({ metaData: this.get('metaData'), usernames: this.get('targetUsernames'), composerTime: this.get('composerTime'), - typingTime: this.get('typingTime') + typingTime: this.get('typingTime'), + tags: this.get('tags') }; this.set('draftStatus', I18n.t('composer.saving_draft_tip')); @@ -848,6 +907,8 @@ Composer.reopenClass({ // The actions the composer can take CREATE_TOPIC, + CREATE_SHARED_DRAFT, + EDIT_SHARED_DRAFT, PRIVATE_MESSAGE, REPLY, EDIT, diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index c26544e7c55..1fe9a1999fb 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -33,13 +33,13 @@ const Group = RestModel.extend({ findMembers(params) { if (Em.isEmpty(this.get('name'))) { return ; } - const self = this, offset = Math.min(this.get("user_count"), Math.max(this.get("offset"), 0)); + const offset = Math.min(this.get("user_count"), Math.max(this.get("offset"), 0)); - return Group.loadMembers(this.get("name"), offset, this.get("limit"), params).then(function (result) { + return Group.loadMembers(this.get("name"), offset, this.get("limit"), params).then(result => { var ownerIds = {}; result.owners.forEach(owner => ownerIds[owner.id] = true); - self.setProperties({ + this.setProperties({ user_count: result.meta.total, limit: result.meta.limit, offset: result.meta.offset, @@ -65,37 +65,45 @@ const Group = RestModel.extend({ }); }, - removeMember(member) { - var self = this; + removeMember(member, params) { return ajax('/groups/' + this.get('id') + '/members.json', { type: "DELETE", data: { user_id: member.get("id") } - }).then(function() { - // reload member list - self.findMembers(); + }).then(() => { + this.findMembers(params); }); }, - addMembers(usernames) { - var self = this; + addMembers(usernames, filter) { return ajax('/groups/' + this.get('id') + '/members.json', { type: "PUT", data: { usernames: usernames } - }).then(function() { - self.findMembers(); + }).then(response => { + if (filter) { + this._filterMembers(response); + } else { + this.findMembers(); + } }); }, - addOwners(usernames) { - var self = this; + addOwners(usernames, filter) { return ajax(`/admin/groups/${this.get('id')}/owners.json`, { type: "PUT", data: { group: { usernames: usernames } } - }).then(function() { - self.findMembers(); + }).then(response => { + if (filter) { + this._filterMembers(response); + } else { + this.findMembers(); + } }); }, + _filterMembers(response) { + return this.findMembers({ filter: response.usernames.join(",") }); + }, + @computed("display_name", "name") displayName(groupDisplayName, name) { return groupDisplayName || name; @@ -116,16 +124,21 @@ const Group = RestModel.extend({ return mentionableLevel === '99'; }, + @computed("visibility_level") + isPrivate(visibilityLevel) { + return visibilityLevel !== 0; + }, + @observes("visibility_level", "canEveryoneMention") _updateAllowMembershipRequests() { - if (this.get('visibility_level') !== 0 || !this.get('canEveryoneMention')) { + if (this.get('isPrivate') || !this.get('canEveryoneMention')) { this.set ('allow_membership_requests', false); } }, @observes("visibility_level") _updatePublic() { - if (this.get('visibility_level') !== 0) { + if (this.get('isPrivate')) { this.set('public', false); this.set('allow_membership_requests', false); } @@ -177,10 +190,7 @@ const Group = RestModel.extend({ }, save() { - const id = this.get('id'); - const url = this.get('is_group_owner') ? `/groups/${id}` : `/admin/groups/${id}`; - - return ajax(url, { + return ajax(`/groups/${this.get('id')}`, { type: "PUT", data: { group: this.asJSON() } }); @@ -242,10 +252,6 @@ Group.reopenClass({ }); }, - find(name) { - return ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group)); - }, - loadMembers(name, offset, limit, params) { return ajax('/groups/' + name + '/members.json', { data: _.extend({ diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 index c125e7bd831..866c657f6b8 100644 --- a/app/assets/javascripts/discourse/models/login-method.js.es6 +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -22,12 +22,21 @@ const LoginMethod = Ember.Object.extend({ let methods; let preRegister; +export const LOGIN_METHODS = [ + "google_oauth2", + "facebook", + "twitter", + "yahoo", + "instagram", + "github" +]; + export function findAll(siteSettings, capabilities, isMobileDevice) { if (methods) { return methods; } methods = []; - [ "google_oauth2", "facebook", "cas", "twitter", "yahoo", "instagram", "github" ].forEach(name => { + LOGIN_METHODS.forEach(name => { if (siteSettings["enable_" + name + "_logins"]) { const params = { name }; if (name === "google_oauth2") { diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index c6903f71fd3..955c3018375 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -168,7 +168,8 @@ const Post = RestModel.extend({ this.setProperties({ deleted_at: new Date(), deleted_by: deletedBy, - can_delete: false + can_delete: false, + can_recover: true }); } else { promise = cookAsync(I18n.t("post.deleted_by_author", {count: Discourse.SiteSettings.delete_removed_posts_after})).then(cooked => { diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6 index 073f6b43f9b..c1e3889176c 100644 --- a/app/assets/javascripts/discourse/models/site.js.es6 +++ b/app/assets/javascripts/discourse/models/site.js.es6 @@ -54,6 +54,11 @@ const Site = RestModel.extend({ return result; }, + @computed + baseUri() { + return Discourse.baseUri; + }, + // Returns it in the correct order, by setting @computed categoriesList() { diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index d198883b58c..efd364ed341 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -107,6 +107,11 @@ export default Ember.Object.extend({ var adapter = this.adapterFor(type); return adapter.find(this, type, findArgs, opts).then(result => { var hydrated = this._hydrateFindResults(result, type, findArgs, opts); + + if (result.extras) { + hydrated.set('extras', result.extras); + } + if (adapter.cache) { const stale = adapter.findStale(this, type, findArgs, opts); hydrated = this._updateStale(stale, hydrated); diff --git a/app/assets/javascripts/discourse/models/tag-group.js.es6 b/app/assets/javascripts/discourse/models/tag-group.js.es6 index cfbd83e1640..ea29f414123 100644 --- a/app/assets/javascripts/discourse/models/tag-group.js.es6 +++ b/app/assets/javascripts/discourse/models/tag-group.js.es6 @@ -1,44 +1,62 @@ -import { ajax } from 'discourse/lib/ajax'; -import RestModel from 'discourse/models/rest'; -import computed from 'ember-addons/ember-computed-decorators'; +import { ajax } from "discourse/lib/ajax"; +import RestModel from "discourse/models/rest"; +import computed from "ember-addons/ember-computed-decorators"; +import PermissionType from "discourse/models/permission-type"; -const TagGroup = RestModel.extend({ - @computed('name', 'tag_names') - disableSave() { - return Ember.isEmpty(this.get('name')) || Ember.isEmpty(this.get('tag_names')) || this.get('saving'); +export default RestModel.extend({ + @computed("name", "tag_names", "saving") + disableSave(name, tagNames, saving) { + return saving || Ember.isEmpty(name) || Ember.isEmpty(tagNames); + }, + + @computed("permissions") + permissionName: { + get(permissions) { + if (!permissions) return "public"; + + if (permissions["everyone"] === PermissionType.FULL) { + return "public"; + } else if (permissions["everyone"] === PermissionType.READONLY) { + return "visible"; + } else { + return "private"; + } + }, + + set(value) { + if (value === "private") { + this.set("permissions", { "staff": PermissionType.FULL }); + } else if (value === "visible") { + this.set("permissions", { "staff": PermissionType.FULL, "everyone": PermissionType.READONLY }); + } else { + this.set("permissions", { "everyone": PermissionType.FULL }); + } + } }, save() { - let url = "/tag_groups"; - const self = this, - isNew = this.get('id') === 'new'; - if (!isNew) { - url = "/tag_groups/" + this.get('id'); - } + this.set("savingStatus", I18n.t("saving")); + this.set("saving", true); - this.set('savingStatus', I18n.t('saving')); - this.set('saving', true); + const isNew = this.get("id") === "new"; + const url = isNew ? "/tag_groups" : `/tag_groups/${this.get("id")}`; + const data = this.getProperties("name", "tag_names", "parent_tag_name", "one_per_topic", "permissions"); return ajax(url, { - data: { - name: this.get('name'), - tag_names: this.get('tag_names'), - parent_tag_name: this.get('parent_tag_name') ? this.get('parent_tag_name') : undefined, - one_per_topic: this.get('one_per_topic') - }, - type: isNew ? 'POST' : 'PUT' - }).then(function(result) { - if(result.tag_group && result.tag_group.id) { - self.set('id', result.tag_group.id); + data, + type: isNew ? "POST" : "PUT" + }).then(result => { + if (result.tag_group && result.tag_group.id) { + this.set("id", result.tag_group.id); } - self.set('savingStatus', I18n.t('saved')); - self.set('saving', false); + }).finally(() => { + this.set("savingStatus", I18n.t("saved")); + this.set("saving", false); }); }, destroy() { - return ajax("/tag_groups/" + this.get('id'), {type: "DELETE"}); + return ajax(`/tag_groups/${this.get("id")}`, { type: "DELETE" }); } }); -export default TagGroup; diff --git a/app/assets/javascripts/discourse/models/tag.js.es6 b/app/assets/javascripts/discourse/models/tag.js.es6 new file mode 100644 index 00000000000..572463e5bde --- /dev/null +++ b/app/assets/javascripts/discourse/models/tag.js.es6 @@ -0,0 +1,9 @@ +import RestModel from "discourse/models/rest"; +import computed from "ember-addons/ember-computed-decorators"; + +export default RestModel.extend({ + @computed("count", "pm_count") + totalCount(count, pmCount) { + return count + pmCount; + } +}); diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6 index 3e39ae5c895..df357eed513 100644 --- a/app/assets/javascripts/discourse/models/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-list.js.es6 @@ -2,6 +2,23 @@ import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import Model from 'discourse/models/model'; +// Whether to show the category badge in topic lists +function displayCategoryInList(site, category) { + if (category) { + if (category.get('has_children')) { + return true; + } + let draftCategoryId = site.get('shared_drafts_category_id'); + if (draftCategoryId && category.get('id') === draftCategoryId) { + return true; + } + + return false; + } + + return true; +} + const TopicList = RestModel.extend({ canLoadMore: Em.computed.notEmpty("more_topics_url"), @@ -34,8 +51,22 @@ const TopicList = RestModel.extend({ loadMore() { if (this.get('loadingMore')) { return Ember.RSVP.resolve(); } - const moreUrl = this.get('more_topics_url'); + let moreUrl = this.get('more_topics_url'); if (moreUrl) { + + let [url, params] = moreUrl.split("?"); + + // ensure we postfix with .json so username paths work + // correctly + if (!url.match(/\.json$/)) { + url += '.json'; + } + + moreUrl = url; + if (params) { + moreUrl += "?" + params; + } + const self = this; this.set('loadingMore', true); @@ -70,17 +101,17 @@ const TopicList = RestModel.extend({ // loads topics with these ids "before" the current topics - loadBefore(topic_ids) { + loadBefore(topic_ids, storeInSession) { const topicList = this, topics = this.get('topics'); // refresh dupes topics.removeObjects(topics.filter(topic => topic_ids.indexOf(topic.get('id')) >= 0)); - const url = `${Discourse.getURL("/")}${this.get('filter')}?topic_ids=${topic_ids.join(",")}`; + const url = `${Discourse.getURL("/")}${this.get('filter')}.json?topic_ids=${topic_ids.join(",")}`; const store = this.store; - return ajax({ url }).then(result => { + return ajax({ url, data: this.get("params") }).then(result => { let i = 0; topicList.forEachNew(TopicList.topicsFrom(store, result), function(t) { // highlight the first of the new topics so we can get a visual feedback @@ -88,22 +119,25 @@ const TopicList = RestModel.extend({ topics.insertAt(i,t); i++; }); - Discourse.Session.currentProp('topicList', topicList); + if (storeInSession) Discourse.Session.currentProp('topicList', topicList); }); } }); TopicList.reopenClass({ - topicsFrom(store, result) { + topicsFrom(store, result, opts) { if (!result) { return; } + opts = opts || {}; + let listKey = opts.listKey || 'topics'; + // Stitch together our side loaded data const categories = Discourse.Category.list(), users = Model.extractByKey(result.users, Discourse.User), groups = Model.extractByKey(result.primary_groups, Ember.Object); - return result.topic_list.topics.map(function (t) { + return result.topic_list[listKey].map(function (t) { t.category = categories.findBy('id', t.category_id); t.posters.forEach(function(p) { p.user = users[p.user_id]; @@ -136,6 +170,10 @@ TopicList.reopenClass({ json.per_page = json.topic_list.per_page; json.topics = this.topicsFrom(store, json); + if (json.topic_list.shared_drafts) { + json.sharedDrafts = this.topicsFrom(store, json, { listKey: 'shared_drafts' }); + } + return json; }, @@ -146,7 +184,7 @@ TopicList.reopenClass({ // hide the category when it has no children hideUniformCategory(list, category) { - list.set('hideCategory', category && !category.get("has_children")); + list.set('hideCategory', !displayCategoryInList(list.site, category)); } }); diff --git a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 index d4c3a0354a3..45fe3f6761e 100644 --- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 @@ -1,6 +1,5 @@ import { NotificationLevels } from 'discourse/lib/notification-levels'; -import computed from "ember-addons/ember-computed-decorators"; -import { on } from "ember-addons/ember-computed-decorators"; +import { default as computed, on } from "ember-addons/ember-computed-decorators"; import { defaultHomepage } from 'discourse/lib/utilities'; import PreloadStore from 'preload-store'; @@ -35,7 +34,7 @@ const TopicTrackingState = Discourse.Model.extend({ tracker.incrementMessageCount(); } - if (data.message_type === "new_topic" || data.message_type === "latest") { + if (["new_topic", "latest"].includes(data.message_type)) { const muted_category_ids = Discourse.User.currentProp("muted_category_ids"); if (_.include(muted_category_ids, data.payload.category_id)) { return; @@ -55,7 +54,7 @@ const TopicTrackingState = Discourse.Model.extend({ tracker.notify(data); } - if (data.message_type === "new_topic" || data.message_type === "unread" || data.message_type === "read") { + if (["new_topic", "unread", "read"].includes(data.message_type)) { tracker.notify(data); const old = tracker.states["t" + data.topic_id]; @@ -117,17 +116,17 @@ const TopicTrackingState = Discourse.Model.extend({ } if (filter === defaultHomepage()) { - const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids"); - if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) { + const suppressed_from_latest_category_ids = Discourse.Site.currentProp("suppressed_from_latest_category_ids"); + if (_.include(suppressed_from_latest_category_ids, data.payload.category_id)) { return; } } - if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") { + if (["all", "latest", "new"].includes(filter) && data.message_type === "new_topic") { this.addIncoming(data.topic_id); } - if ((filter === "all" || filter === "unread") && data.message_type === "unread") { + if (["all", "unread"].includes(filter) && data.message_type === "unread") { const old = this.states["t" + data.topic_id]; if(!old || old.highest_post_number === old.last_read_post_number) { this.addIncoming(data.topic_id); diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 69a4f9c27f2..bf37af7917e 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -3,6 +3,7 @@ import { flushMap } from 'discourse/models/store'; import RestModel from 'discourse/models/rest'; import { propertyEqual } from 'discourse/lib/computed'; import { longDate } from 'discourse/lib/formatter'; +import { isRTL } from 'discourse/lib/text-direction'; import computed from 'ember-addons/ember-computed-decorators'; import ActionSummary from 'discourse/models/action-summary'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -58,7 +59,13 @@ const Topic = RestModel.extend({ @computed('fancy_title') fancyTitle(title) { - return censor(emojiUnescape(title || ""), Discourse.Site.currentProp('censored_words')); + let fancyTitle = censor(emojiUnescape(title || ""), Discourse.Site.currentProp('censored_words')); + + if (Discourse.SiteSettings.support_mixed_text_direction) { + let titleDir = isRTL(title) ? 'rtl' : 'ltr'; + return `${fancyTitle}`; + } + return fancyTitle; }, // returns createdAt if there's no bumped date @@ -94,7 +101,7 @@ const Topic = RestModel.extend({ const newTags = []; tags.forEach(function(tag){ - if (title.toLowerCase().indexOf(tag) === -1 || Discourse.SiteSettings.staff_tags.indexOf(tag) !== -1) { + if (title.toLowerCase().indexOf(tag) === -1) { newTags.push(tag); } }); @@ -348,7 +355,7 @@ const Topic = RestModel.extend({ 'details.can_delete': false, 'details.can_recover': true }); - return ajax("/t/" + this.get('id'), { + return ajax(`/t/${this.get('id')}`, { data: { context: window.location.pathname }, type: 'DELETE' }); @@ -362,7 +369,10 @@ const Topic = RestModel.extend({ 'details.can_delete': true, 'details.can_recover': false }); - return ajax("/t/" + this.get('id') + "/recover", { type: 'PUT' }); + return ajax(`/t/${this.get('id')}/recover`, { + data: { context: window.location.pathname }, + type: 'PUT' + }); }, // Update our attributes from a JSON result @@ -469,6 +479,23 @@ const Topic = RestModel.extend({ return promise; }, + publish() { + return ajax(`/t/${this.get('id')}/publish`, { + type: 'PUT', + data: this.getProperties('destination_category_id') + }).then(() => { + this.set('destination_category_id', null); + }).catch(popupAjaxError); + }, + + updateDestinationCategory(categoryId) { + this.set('destination_category_id', categoryId); + return ajax(`/t/${this.get('id')}/shared-draft`, { + method: 'PUT', + data: { category_id: categoryId } + }); + }, + convertTopic(type) { return ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => { window.location.reload(); diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 9925d4798a0..a76f17be969 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -23,14 +23,16 @@ const User = RestModel.extend({ hasPMs: Em.computed.gt("private_messages_stats.all", 0), hasStartedPMs: Em.computed.gt("private_messages_stats.mine", 0), hasUnreadPMs: Em.computed.gt("private_messages_stats.unread", 0), - hasPosted: Em.computed.gt("post_count", 0), - hasNotPosted: Em.computed.not("hasPosted"), - canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"), redirected_to_top: { reason: null, }, + @computed("can_be_deleted", "post_count") + canBeDeleted(canBeDeleted, postCount) { + return canBeDeleted && postCount <= 5; + }, + @computed() stream() { return UserStream.create({ user: this }); @@ -167,8 +169,6 @@ const User = RestModel.extend({ isElder: Em.computed.equal('trust_level', 4), canManageTopic: Em.computed.or('staff', 'isElder'), - isSuspended: Em.computed.equal('suspended', true), - @computed("previous_visit_at") previousVisitAt(previous_visit_at) { return new Date(previous_visit_at); @@ -216,6 +216,7 @@ const User = RestModel.extend({ 'website', 'location', 'name', + 'title', 'locale', 'custom_fields', 'user_fields', @@ -281,6 +282,12 @@ const User = RestModel.extend({ } }); + ['muted_tags', 'tracked_tags', 'watched_tags', 'watching_first_post_tags'].forEach(prop => { + if (fields === undefined || fields.includes(prop)) { + data[prop] = this.get(prop) ? this.get(prop).join(',') : ''; + } + }); + // TODO: We can remove this when migrated fully to rest model. this.set('isSaving', true); return ajax(userPath(`${this.get('username_lower')}.json`), { @@ -304,6 +311,20 @@ const User = RestModel.extend({ }); }, + loadSecondFactorCodes(password) { + return ajax("/u/second_factors.json", { + data: { password }, + type: 'POST' + }); + }, + + toggleSecondFactor(token, enable) { + return ajax("/u/second_factor.json", { + data: { second_factor_token: token, enable }, + type: 'PUT' + }); + }, + loadUserAction(id) { const stream = this.get('stream'); return ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => { @@ -326,13 +347,26 @@ const User = RestModel.extend({ ua.action_type === UserAction.TYPES.topics; }, + numGroupsToDisplay: 2, + @computed("groups.[]") - displayGroups() { + filteredGroups() { const groups = this.get('groups') || []; - const filtered = groups.filter(group => { + + return groups.filter(group => { return !group.automatic || group.name === "moderators"; }); - return filtered.length === 0 ? null : filtered; + }, + + @computed("filteredGroups", "numGroupsToDisplay") + displayGroups(filteredGroups, numGroupsToDisplay) { + const groups = filteredGroups.slice(0, numGroupsToDisplay); + return groups.length === 0 ? null : groups; + }, + + @computed("filteredGroups", "numGroupsToDisplay") + showMoreGroupsLink(filteredGroups, numGroupsToDisplay) { + return filteredGroups.length > numGroupsToDisplay; }, // The user's stat count, excluding PMs. @@ -534,6 +568,25 @@ const User = RestModel.extend({ canManageGroup(group) { return group.get('automatic') ? false : (this.get('admin') || group.get('is_group_owner')); + }, + + @computed('groups.@each.title', 'badges.@each') + availableTitles() { + let titles = []; + + _.each(this.get('groups'), group => { + if (group.get('title')) { + titles.push(group.get('title')); + } + }); + + _.each(this.get('badges'), badge => { + if (badge.get('allow_title')) { + titles.push(badge.get('name')); + } + }); + + return _.uniq(titles).sort(); } }); diff --git a/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6 b/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6 index 40faddcde29..6d35e0800fc 100644 --- a/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6 +++ b/app/assets/javascripts/discourse/raw-views/list/post-count-or-badges.js.es6 @@ -1,4 +1,11 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; + export default Ember.Object.extend({ postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts'), - showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent') + showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent'), + + @computed + newDotText() { + return (this.currentUser && this.currentUser.trust_level > 0) ? "" : I18n.t('filters.new.lower_title'); + } }); diff --git a/app/assets/javascripts/discourse/raw-views/topic-status.js.es6 b/app/assets/javascripts/discourse/raw-views/topic-status.js.es6 index 8b6af94cbb3..cf9f81cc0cb 100644 --- a/app/assets/javascripts/discourse/raw-views/topic-status.js.es6 +++ b/app/assets/javascripts/discourse/raw-views/topic-status.js.es6 @@ -53,7 +53,7 @@ export default Ember.Object.extend({ results.forEach(result => { result.title = I18n.t(`topic_statuses.${result.key}.help`); - if (!self.disableActions && (result.key === "pinned" ||result.key === "unpinned")) { + if (this.currentUser && (result.key === "pinned" || result.key === "unpinned")) { result.openTag = 'a href'; result.closeTag = 'a'; } else { diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 72dca540a18..a2f1dfe3dbe 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -5,6 +5,8 @@ export default function() { this.route('about', { path: '/about', resetNamespace: true }); + this.route('post', { path: '/p/:id' }); + // Topic routes this.route('topic', { path: '/t/:slug/:id', resetNamespace: true }, function() { this.route('fromParams', { path: '/' }); @@ -12,7 +14,6 @@ export default function() { }); this.route('topicBySlugOrId', { path: '/t/:slugOrId', resetNamespace: true }); - this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' }); this.route('discovery', { path: '/', resetNamespace: true }, function() { // top @@ -47,7 +48,9 @@ export default function() { this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' }); }); - this.route('groups', { resetNamespace: true }); + this.route('groups', { resetNamespace: true }, function() { + this.route("new", { path: "custom/new" }); + }); this.route('group', { path: '/groups/:name', resetNamespace: true }, function() { this.route('members'); @@ -56,11 +59,20 @@ export default function() { this.route('posts'); this.route('topics'); this.route('mentions'); - this.route('messages'); }); - this.route('logs'); - this.route('edit'); + this.route('manage', function() { + this.route('profile'); + this.route('membership'); + this.route('interaction'); + this.route('members'); + this.route('logs'); + }); + + this.route('messages', function() { + this.route('inbox'); + this.route('archive'); + }); }); // User routes @@ -96,6 +108,8 @@ export default function() { this.route('archive'); this.route('group', { path: 'group/:name'}); this.route('groupArchive', { path: 'group/:name/archive'}); + this.route('tags'); + this.route('tagsShow', { path: 'tags/:id'}); }); this.route('preferences', { resetNamespace: true }, function() { @@ -110,9 +124,8 @@ export default function() { this.route('username'); this.route('email'); + this.route('second-factor'); this.route('about', { path: '/about-me' }); - this.route('badgeTitle', { path: '/badge_title' }); - this.route('card-badge', { path: '/card-badge' }); }); this.route('userInvited', { path: '/invited', resetNamespace: true }, function() { diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index 4b29783e565..fb4f1b7fe83 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -118,8 +118,6 @@ export default (filterArg, params) => { this.controllerFor('discovery/topics').setProperties(topicOpts); this.searchService.set('searchContext', category.get('searchContext')); this.set('topics', null); - - this.openTopicDraft(topics); }, renderTemplate() { diff --git a/app/assets/javascripts/discourse/routes/build-group-messages-route.js.es6 b/app/assets/javascripts/discourse/routes/build-group-messages-route.js.es6 new file mode 100644 index 00000000000..460134ee8de --- /dev/null +++ b/app/assets/javascripts/discourse/routes/build-group-messages-route.js.es6 @@ -0,0 +1,35 @@ +import UserTopicListRoute from "discourse/routes/user-topic-list"; + +export default (type) => { + return UserTopicListRoute.extend({ + titleToken() { + return I18n.t(`user.messages.${type}`); + }, + + model() { + const groupName = this.modelFor('group').get('name'); + const username = this.currentUser.get("username_lower"); + let filter = `topics/private-messages-group/${username}/${groupName}`; + if (this._isArchive()) filter = `${filter}/archive`; + return this.store.findFiltered("topicList", { filter }); + }, + + setupController() { + this._super.apply(this, arguments); + + const groupName = this.modelFor('group').get('name'); + let channel = `/private-messages/group/${groupName}`; + if (this._isArchive()) channel = `${channel}/archive`; + this.controllerFor("user-topics-list").subscribe(channel); + + this.controllerFor("user-topics-list").setProperties({ + hideCategory: true, + showPosters: true + }); + }, + + _isArchive() { + return type === 'archive'; + }, + }); +}; diff --git a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 index b20e9e1b3dc..4be8889dea9 100644 --- a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 @@ -1,10 +1,15 @@ import UserTopicListRoute from "discourse/routes/user-topic-list"; // A helper to build a user topic list route -export default (viewName, path) => { +export default (viewName, path, channel) => { return UserTopicListRoute.extend({ userActionType: Discourse.UserAction.TYPES.messages_received, + titleToken() { + const key = viewName === "index" ? "inbox" : viewName; + return [I18n.t(`user.messages.${key}`), I18n.t("user.private_messages")]; + }, + actions: { didTransition() { this.controllerFor("user-topics-list")._showFooter(); @@ -19,19 +24,30 @@ export default (viewName, path) => { setupController() { this._super.apply(this, arguments); + if (channel) { + this.controllerFor("user-topics-list").subscribe(`/private-messages/${channel}`); + } + this.controllerFor("user-topics-list").setProperties({ hideCategory: true, showPosters: true, canBulkSelect: true, + tagsForUser: this.modelFor("user").get("username_lower"), selected: [] }); - this.controllerFor("user-private-messages").set("archive", false); - this.controllerFor("user-private-messages").set("pmView", viewName); + this.controllerFor("user-private-messages").setProperties({ + archive: false, + pmView: viewName, + showToggleBulkSelect: true + }); + this.searchService.set('contextType', 'private_messages'); }, deactivate() { + this.controllerFor('user-topics-list').unsubscribe(); + this.searchService.set( 'searchContext', this.controllerFor("user").get("model.searchContext") diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index d7a2116bee6..198ab74e49f 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -54,8 +54,8 @@ function findTopicList(store, tracking, filter, filterParams, extras) { tracking.trackIncoming(list.filter); } Discourse.Session.currentProp('topicList', list); - if (list.topic_list && list.topic_list.tags) { - Discourse.Site.currentProp('top_tags', list.topic_list.tags); + if (list.topic_list && list.topic_list.top_tags) { + Discourse.Site.currentProp('top_tags', list.topic_list.top_tags); } return list; }); @@ -91,7 +91,7 @@ export default function(filter, extras) { const topicOpts = { model, category: null, - period: model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''), + period: model.get('for_period') || (filter.indexOf('top/') >= 0 ? filter.split('/')[1] : ''), selected: [], expandGloballyPinned: true }; @@ -106,8 +106,6 @@ export default function(filter, extras) { } } this.controllerFor('discovery/topics').setProperties(topicOpts); - - this.openTopicDraft(model); this.controllerFor('navigation/default').set('canCreateTopic', model.get('can_create_topic')); }, diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index 8b3d3bbc310..f726cb5d8f1 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -1,15 +1,23 @@ import Composer from 'discourse/models/composer'; const DiscourseRoute = Ember.Route.extend({ + showFooter: false, // Set to true to refresh a model without a transition if a query param // changes resfreshQueryWithoutTransition: false, + activate() { + this._super(); + if (this.get('showFooter')) { + this.controllerFor('application').set('showFooter', true); + } + }, + refresh() { if (!this.refreshQueryWithoutTransition) { return this._super(); } - if (!this.router.router.activeTransition) { + if (!this.router._routerMicrolib.activeTransition) { const controller = this.controller, model = controller.get('model'), params = this.controller.getProperties(Object.keys(this.queryParams)); @@ -44,7 +52,25 @@ const DiscourseRoute = Ember.Route.extend({ refreshTitle() { Ember.run.once(this, this._refreshTitleOnce); + }, + + clearTopicDraft() { + // perhaps re-delegate this to root controller in all cases? + // TODO also poison the store so it does not come back from the + // dead + if (this.get('controller.list.draft')) { + this.set('controller.list.draft', null); + } + + if (this.controllerFor("discovery/categories").get('model.draft')) { + this.controllerFor("discovery/categories").set('model.draft', null); + } + + if (this.controllerFor("discovery/topics").get('model.draft')) { + this.controllerFor("discovery/topics").set('model.draft', null); + } } + }, redirectIfLoginRequired() { @@ -55,17 +81,18 @@ const DiscourseRoute = Ember.Route.extend({ }, openTopicDraft(model){ - // If there's a draft, open the create topic composer - if (model.draft) { - const composer = this.controllerFor('composer'); - if (!composer.get('model.viewOpen')) { - composer.open({ - action: Composer.CREATE_TOPIC, - draft: model.draft, - draftKey: model.draft_key, - draftSequence: model.draft_sequence - }); - } + const composer = this.controllerFor('composer'); + + if (composer.get('model.action') === Composer.CREATE_TOPIC && + composer.get('model.draftKey') === model.draft_key) { + composer.set('model.composeState', Composer.OPEN); + } else { + composer.open({ + action: Composer.CREATE_TOPIC, + draft: model.draft, + draftKey: model.draft_key, + draftSequence: model.draft_sequence + }); } }, diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 index 0ba2194e501..a85c0743f66 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 @@ -12,20 +12,24 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { this.render("discovery/categories", { outlet: "list-container" }); }, - model() { - const style = !this.site.mobileView && this.siteSettings.desktop_category_page_style; - const parentCategory = this.get("model.parentCategory"); + findCategories() { + let style = !this.site.mobileView && + this.siteSettings.desktop_category_page_style; - let promise; + let parentCategory = this.get("model.parentCategory"); if (parentCategory) { - promise = CategoryList.listForParent(this.store, parentCategory); + return CategoryList.listForParent(this.store, parentCategory); } else if (style === "categories_and_latest_topics") { - promise = this._loadCategoriesAndLatestTopics(); - } else { - promise = CategoryList.list(this.store); + return this._findCategoriesAndTopics('latest'); + } else if (style === "categories_and_top_topics") { + return this._findCategoriesAndTopics('top'); } - return promise.then(model => { + return CategoryList.list(this.store); + }, + + model() { + return this.findCategories().then(model => { const tracking = this.topicTrackingState; if (tracking) { tracking.sync(model, "categories"); @@ -35,26 +39,31 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { }); }, - _loadCategoriesAndLatestTopics() { - const wrappedCategoriesList = PreloadStore.getAndRemove("categories_list"); - const topicListLatest = PreloadStore.getAndRemove("topic_list_latest"); - const categoriesList = wrappedCategoriesList && wrappedCategoriesList.category_list; - if (categoriesList && topicListLatest) { - return new Ember.RSVP.Promise(resolve => { - const result = Ember.Object.create({ - categories: CategoryList.categoriesFrom(this.store, wrappedCategoriesList), - topics: TopicList.topicsFrom(this.store, topicListLatest), + _findCategoriesAndTopics(filter) { + return Ember.RSVP.hash({ + wrappedCategoriesList: PreloadStore.getAndRemove("categories_list"), + topicsList: PreloadStore.getAndRemove(`topic_list_${filter}`) + }).then(hash => { + let { wrappedCategoriesList, topicsList } = hash; + let categoriesList = wrappedCategoriesList && + wrappedCategoriesList.category_list; + + if (categoriesList && topicsList) { + return Ember.Object.create({ + categories: CategoryList.categoriesFrom( + this.store, + wrappedCategoriesList + ), + topics: TopicList.topicsFrom(this.store, topicsList), can_create_category: categoriesList.can_create_category, can_create_topic: categoriesList.can_create_topic, draft_key: categoriesList.draft_key, draft: categoriesList.draft, draft_sequence: categoriesList.draft_sequence }); - - resolve(result); - }); - } else { - return ajax("/categories_and_latest").then(result => { + } + // Otherwise, return the ajax result + return ajax(`/categories_and_${filter}`).then(result => { return Ember.Object.create({ categories: CategoryList.categoriesFrom(this.store, result), topics: TopicList.topicsFrom(this.store, result), @@ -65,7 +74,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { draft_sequence: result.category_list.draft_sequence }); }); - } + }); }, titleToken() { @@ -80,8 +89,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { showCategoryAdmin: model.get("can_create_category"), canCreateTopic: model.get("can_create_topic"), }); - - this.openTopicDraft(model); }, actions: { @@ -124,7 +131,12 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { }, createTopic() { - this.openComposer(this.controllerFor("discovery/categories")); + const model = this.controllerFor("discovery/categories").get('model'); + if (model.draft) { + this.openTopicDraft(model); + } else { + this.openComposer(this.controllerFor("discovery/categories")); + } }, didTransition() { diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6 index a77806d1a4d..b8b05fe35e6 100644 --- a/app/assets/javascripts/discourse/routes/discovery.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery.js.es6 @@ -45,7 +45,12 @@ export default Discourse.Route.extend(OpenComposer, { }, createTopic() { - this.openComposer(this.controllerFor("discovery/topics")); + const model = this.controllerFor("discovery/topics").get('model'); + if (model.draft) { + this.openTopicDraft(model); + } else { + this.openComposer(this.controllerFor("discovery/topics")); + } }, dismissReadTopics(dismissTopics) { diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 index 73598c948ef..3d981720ac4 100644 --- a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 @@ -36,7 +36,8 @@ export default Discourse.Route.extend({ return null; } }).then(results => { - const model = (results && translateResults(results)) || {}; + const grouped_search_result = results ? results.grouped_search_result : {}; + const model = (results && translateResults(results)) || { grouped_search_result }; setTransient('lastSearch', { searchKey, model }, 5); return model; }); diff --git a/app/assets/javascripts/discourse/routes/group-activity-messages.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-messages.js.es6 deleted file mode 100644 index 660a5c7cd5f..00000000000 --- a/app/assets/javascripts/discourse/routes/group-activity-messages.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import { buildGroupPage } from 'discourse/routes/group-activity-posts'; - -export default buildGroupPage('messages'); diff --git a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 index 827c8c468b6..bc0e3d2ccf7 100644 --- a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 @@ -12,8 +12,13 @@ export function buildGroupPage(type) { }, setupController(controller, model) { - this.controllerFor('group-activity-posts').setProperties({ model, type, canLoadMore: true }); - this.controllerFor("group").set("showing", type); + let loadedAll = model.length < 20; + this.controllerFor('group-activity-posts').setProperties({ + model, + type, + canLoadMore: !loadedAll, + }); + this.controllerFor('application').set('showFooter', loadedAll); }, renderTemplate() { diff --git a/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6 index 16164a51bb3..5310f0cad65 100644 --- a/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-activity-topics.js.es6 @@ -1,3 +1,11 @@ -import { buildGroupPage } from 'discourse/routes/group-activity-posts'; +export default Discourse.Route.extend({ + titleToken() { + return I18n.t(`groups.topics`); + }, -export default buildGroupPage('topics'); + model() { + return this.store.findFiltered("topicList", { + filter: `topics/groups/${this.modelFor('group').get('name')}` + }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/group-edit.js.es6 b/app/assets/javascripts/discourse/routes/group-edit.js.es6 deleted file mode 100644 index 19b8ec1caee..00000000000 --- a/app/assets/javascripts/discourse/routes/group-edit.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -export default Ember.Route.extend({ - titleToken() { - return I18n.t('groups.edit.title'); - }, - - model() { - return this.modelFor('group'); - }, - - afterModel(group) { - if (!this.currentUser || !this.currentUser.canManageGroup(group)) { - this.transitionTo("group.members", group); - } - }, - - setupController(controller, model) { - this.controllerFor('group-edit').setProperties({ model }); - this.controllerFor("group").set("showing", 'edit'); - model.findMembers(); - } -}); diff --git a/app/assets/javascripts/discourse/routes/group-index.js.es6 b/app/assets/javascripts/discourse/routes/group-index.js.es6 index 003a4a8d9e1..2792291dc31 100644 --- a/app/assets/javascripts/discourse/routes/group-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-index.js.es6 @@ -1,15 +1,38 @@ +import showModal from 'discourse/lib/show-modal'; + export default Discourse.Route.extend({ titleToken() { - return I18n.t('groups.members'); + return I18n.t('groups.members.title'); }, - model() { + model(params) { + this._params = params; return this.modelFor("group"); }, setupController(controller, model) { this.controllerFor("group").set("showing", "members"); - controller.set("model", model); + + controller.setProperties({ + model, + filterInput: this._params.filter + }); + controller.refreshMembers(); + }, + + actions: { + showAddMembersModal() { + showModal('group-add-members', { model: this.modelFor('group') }); + }, + + showBulkAddModal() { + showModal('group-bulk-add', { model: this.modelFor('group') }); + }, + + didTransition() { + this.controllerFor("group-index").set("filterInput", this._params.filter); + return true; + } } }); diff --git a/app/assets/javascripts/discourse/routes/group-logs.js.es6 b/app/assets/javascripts/discourse/routes/group-logs.js.es6 deleted file mode 100644 index 0d365fab188..00000000000 --- a/app/assets/javascripts/discourse/routes/group-logs.js.es6 +++ /dev/null @@ -1,20 +0,0 @@ -export default Ember.Route.extend({ - titleToken() { - return I18n.t('groups.logs'); - }, - - model() { - return this.modelFor('group').findLogs(); - }, - - setupController(controller, model) { - this.controllerFor('group-logs').setProperties({ model }); - this.controllerFor("group").set("showing", 'logs'); - }, - - actions: { - willTransition() { - this.controllerFor('group-logs').reset(); - } - } -}); diff --git a/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 new file mode 100644 index 00000000000..590722bb0fa --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 @@ -0,0 +1,7 @@ +export default Discourse.Route.extend({ + showFooter: true, + + beforeModel() { + this.transitionTo("group.manage.profile"); + } +}); diff --git a/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6 new file mode 100644 index 00000000000..313fcda6423 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-interaction.js.es6 @@ -0,0 +1,7 @@ +export default Discourse.Route.extend({ + showFooter: true, + + titleToken() { + return I18n.t('groups.manage.interaction.title'); + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 new file mode 100644 index 00000000000..85ee4dfae98 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 @@ -0,0 +1,19 @@ +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('groups.manage.logs.title'); + }, + + model() { + return this.modelFor('group').findLogs(); + }, + + setupController(controller, model) { + this.controllerFor('group-manage-logs').setProperties({ model }); + }, + + actions: { + willTransition() { + this.controllerFor('group-manage-logs').reset(); + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6 new file mode 100644 index 00000000000..7689197d96a --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-membership.js.es6 @@ -0,0 +1,13 @@ +export default Discourse.Route.extend({ + showFooter: true, + + titleToken() { + return I18n.t('groups.manage.membership.title'); + }, + + afterModel(group) { + if (group.get('automatic')) { + this.replaceWith("group.manage.interaction", group); + } + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 new file mode 100644 index 00000000000..e0c37246e16 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 @@ -0,0 +1,13 @@ +export default Discourse.Route.extend({ + showFooter: true, + + titleToken() { + return I18n.t('groups.manage.profile.title'); + }, + + afterModel(group) { + if (group.get('automatic')) { + this.replaceWith("group.manage.interaction", group); + } + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group-manage.js.es6 b/app/assets/javascripts/discourse/routes/group-manage.js.es6 new file mode 100644 index 00000000000..e7af6740ed3 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage.js.es6 @@ -0,0 +1,25 @@ +export default Discourse.Route.extend({ + showFooter: true, + + titleToken() { + return I18n.t('groups.manage.title'); + }, + + model() { + return this.modelFor('group'); + }, + + afterModel(group) { + if (!this.currentUser || + !(this.currentUser.admin && group.get('automatic')) && + !this.currentUser.canManageGroup(group)) { + + this.transitionTo("group.members", group); + } + }, + + setupController(controller, model) { + this.controllerFor('group-manage').setProperties({ model }); + this.controllerFor("group").set("showing", 'manage'); + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group-messages-archive.js.es6 b/app/assets/javascripts/discourse/routes/group-messages-archive.js.es6 new file mode 100644 index 00000000000..4920aa20005 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-messages-archive.js.es6 @@ -0,0 +1,3 @@ +import buildGroupMessagesRoute from 'discourse/routes/build-group-messages-route'; + +export default buildGroupMessagesRoute('archive'); diff --git a/app/assets/javascripts/discourse/routes/group-messages-inbox.js.es6 b/app/assets/javascripts/discourse/routes/group-messages-inbox.js.es6 new file mode 100644 index 00000000000..03ad513b5d2 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-messages-inbox.js.es6 @@ -0,0 +1,3 @@ +import buildGroupMessagesRoute from 'discourse/routes/build-group-messages-route'; + +export default buildGroupMessagesRoute('inbox'); diff --git a/app/assets/javascripts/discourse/routes/group-messages-index.js.es6 b/app/assets/javascripts/discourse/routes/group-messages-index.js.es6 new file mode 100644 index 00000000000..68750d154a0 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-messages-index.js.es6 @@ -0,0 +1,5 @@ +export default Ember.Route.extend({ + beforeModel: function() { + this.transitionTo("group.messages.inbox"); + } +}); diff --git a/app/assets/javascripts/discourse/routes/group-messages.js.es6 b/app/assets/javascripts/discourse/routes/group-messages.js.es6 new file mode 100644 index 00000000000..2bf743b726d --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-messages.js.es6 @@ -0,0 +1,15 @@ +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('groups.messages'); + }, + + model() { + return this.modelFor('group'); + }, + + afterModel(group) { + if (!group.get('is_group_user') && !(this.currentUser && this.currentUser.admin)) { + this.transitionTo("group.members", group); + } + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group.js.es6 b/app/assets/javascripts/discourse/routes/group.js.es6 index 3df20f794bf..11926198cca 100644 --- a/app/assets/javascripts/discourse/routes/group.js.es6 +++ b/app/assets/javascripts/discourse/routes/group.js.es6 @@ -1,13 +1,10 @@ -import Group from 'discourse/models/group'; - export default Discourse.Route.extend({ - titleToken() { return [ this.modelFor('group').get('name') ]; }, model(params) { - return Group.find(params.name); + return this.store.find("group", params.name); }, serialize(model) { @@ -15,6 +12,6 @@ export default Discourse.Route.extend({ }, setupController(controller, model) { - controller.setProperties({ model, counts: this.get('counts') }); + controller.setProperties({ model }); } }); diff --git a/app/assets/javascripts/discourse/routes/groups-index.js.es6 b/app/assets/javascripts/discourse/routes/groups-index.js.es6 new file mode 100644 index 00000000000..9b53f67906d --- /dev/null +++ b/app/assets/javascripts/discourse/routes/groups-index.js.es6 @@ -0,0 +1,27 @@ +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('groups.index.title'); + }, + + queryParams: { + order: { refreshModel: true, replace: true }, + asc: { refreshModel: true, replace: true }, + filter: { refreshModel: true }, + type: { refreshModel: true, replace: true }, + username: { refreshModel: true }, + }, + + refreshQueryWithoutTransition: true, + + model(params) { + this._params = params; + return this.store.findAll("group", params); + }, + + setupController(controller, model) { + controller.setProperties({ + model, + filterInput: this._params.filter + }); + }, +}); diff --git a/app/assets/javascripts/discourse/routes/groups-new.js.es6 b/app/assets/javascripts/discourse/routes/groups-new.js.es6 new file mode 100644 index 00000000000..e5a6fdd03c4 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/groups-new.js.es6 @@ -0,0 +1,23 @@ +import Group from 'discourse/models/group'; + +export default Discourse.Route.extend({ + showFooter: true, + + titleToken() { + return I18n.t('admin.groups.new.title'); + }, + + model() { + return Group.create({ automatic: false, visibility_level: 0 }); + }, + + setupController(controller, model) { + controller.set("model", model); + }, + + afterModel() { + if (!(this.currentUser && this.currentUser.admin)) { + this.transitionTo("groups"); + } + }, +}); diff --git a/app/assets/javascripts/discourse/routes/groups.js.es6 b/app/assets/javascripts/discourse/routes/groups.js.es6 deleted file mode 100644 index 582ebe0f5c6..00000000000 --- a/app/assets/javascripts/discourse/routes/groups.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -export default Discourse.Route.extend({ - titleToken() { - return I18n.t('groups.index.title'); - }, - - model(params) { - return this.store.findAll('group', params); - }, - - setupController(controller, model) { - controller.set('groups', model); - } -}); diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6 index 91269c4a5b0..d3ea4fde433 100644 --- a/app/assets/javascripts/discourse/routes/new-message.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-message.js.es6 @@ -4,10 +4,12 @@ import Group from 'discourse/models/group'; export default Discourse.Route.extend({ beforeModel(transition) { + const self = this; const params = transition.queryParams; + const groupName = params.groupname || params.group_name; - if (this.currentUser) { - this.replaceWith("discovery.latest").then(e => { + if (self.currentUser) { + self.replaceWith("discovery.latest").then(e => { if (params.username) { // send a message to a user User.findByUsername(params.username).then(user => { @@ -19,13 +21,13 @@ export default Discourse.Route.extend({ }).catch(function() { bootbox.alert(I18n.t("generic_error")); }); - } else if (params.groupname) { + } else if (groupName) { // send a message to a group - Group.messageable(params.groupname).then(result => { + Group.messageable(groupName).then(result => { if (result.messageable) { - Ember.run.next(() => e.send("createNewMessageViaParams", params.groupname, params.title, params.body)); + Ember.run.next(() => e.send("createNewMessageViaParams", groupName, params.title, params.body)); } else { - bootbox.alert(I18n.t("composer.cant_send_pm", { username: params.groupname })); + bootbox.alert(I18n.t("composer.cant_send_pm", { username: groupName })); } }).catch(function() { bootbox.alert(I18n.t("generic_error")); diff --git a/app/assets/javascripts/discourse/routes/post.js.es6 b/app/assets/javascripts/discourse/routes/post.js.es6 new file mode 100644 index 00000000000..f8f06e46c43 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/post.js.es6 @@ -0,0 +1,9 @@ +import { ajax } from "discourse/lib/ajax"; + +export default Discourse.Route.extend({ + beforeModel({ params }) { + return ajax(`/p/${params.post.id}`).then(t => { + this.transitionTo("topic.fromParamsNear", t.slug, t.id, t.current_post_number); + }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/preferences-about.js.es6 b/app/assets/javascripts/discourse/routes/preferences-about.js.es6 index 12e819af59a..e5c86f6fdb4 100644 --- a/app/assets/javascripts/discourse/routes/preferences-about.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-about.js.es6 @@ -1,6 +1,8 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ + showFooter: true, + model: function() { return this.modelFor('user'); }, diff --git a/app/assets/javascripts/discourse/routes/preferences-account.js.es6 b/app/assets/javascripts/discourse/routes/preferences-account.js.es6 index 2c34c9df2f0..29200b6fd11 100644 --- a/app/assets/javascripts/discourse/routes/preferences-account.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-account.js.es6 @@ -1,11 +1,27 @@ +import UserBadge from 'discourse/models/user-badge'; import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ + showFooter: true, + + model: function() { + const user = this.modelFor('user'); + if (this.siteSettings.enable_badges) { + return UserBadge.findByUsername(this.modelFor('user').get('username')).then(userBadges => { + user.set('badges', userBadges.map(ub => ub.badge)); + return user; + }); + } else { + return user; + } + }, + setupController(controller, user) { controller.reset(); controller.setProperties({ model: user, - newNameInput: user.get('name') + newNameInput: user.get('name'), + newTitleInput: user.get('title') }); } }); diff --git a/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 b/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 deleted file mode 100644 index 4491aff2543..00000000000 --- a/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 +++ /dev/null @@ -1,32 +0,0 @@ -import UserBadge from 'discourse/models/user-badge'; -import RestrictedUserRoute from "discourse/routes/restricted-user"; - -export default RestrictedUserRoute.extend({ - model: function() { - return UserBadge.findByUsername(this.modelFor('user').get('username')); - }, - - renderTemplate: function() { - return this.render('user/badge-title', { into: 'user' }); - }, - - // A bit odd, but if we leave to /preferences we need to re-render that outlet - deactivate: function() { - this._super(); - this.render('preferences', { into: 'user', controller: 'preferences' }); - }, - - setupController: function(controller, model) { - controller.set('model', model); - controller.set('user', this.modelFor('user')); - - model.forEach(function(userBadge) { - if (userBadge.get('badge.name') === controller.get('user.title')) { - controller.set('selectedUserBadgeId', userBadge.get('id')); - } - }); - if (!controller.get('selectedUserBadgeId') && controller.get('selectableUserBadges.length') > 0) { - controller.set('selectedUserBadgeId', controller.get('selectableUserBadges')[0].get('id')); - } - } -}); diff --git a/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 b/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 deleted file mode 100644 index 8ec81a95d1c..00000000000 --- a/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import UserBadge from 'discourse/models/user-badge'; -import RestrictedUserRoute from "discourse/routes/restricted-user"; - -export default RestrictedUserRoute.extend({ - model: function() { - return UserBadge.findByUsername(this.modelFor('user').get('username')); - }, - - renderTemplate: function() { - return this.render({ into: 'user' }); - }, - - // A bit odd, but if we leave to /preferences we need to re-render that outlet - deactivate: function() { - this._super(); - this.render('preferences', { into: 'user', controller: 'preferences' }); - }, - - setupController: function(controller, model) { - controller.set('model', model); - controller.set('user', this.modelFor('user')); - - model.forEach(function(userBadge) { - if (userBadge.get('badge.image') === controller.get('user.card_image_badge')) { - controller.set('selectedUserBadgeId', userBadge.get('id')); - } - }); - } -}); - diff --git a/app/assets/javascripts/discourse/routes/preferences-categories.js.es6 b/app/assets/javascripts/discourse/routes/preferences-categories.js.es6 new file mode 100644 index 00000000000..713d79e4207 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-categories.js.es6 @@ -0,0 +1,5 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + showFooter: true +}); diff --git a/app/assets/javascripts/discourse/routes/preferences-email.js.es6 b/app/assets/javascripts/discourse/routes/preferences-email.js.es6 index b9c02c3a850..77e80e75764 100644 --- a/app/assets/javascripts/discourse/routes/preferences-email.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-email.js.es6 @@ -1,6 +1,8 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ + showFooter: true, + model: function() { return this.modelFor('user'); }, diff --git a/app/assets/javascripts/discourse/routes/preferences-index.js.es6 b/app/assets/javascripts/discourse/routes/preferences-index.js.es6 index 3006f4b8533..49964f42cc2 100644 --- a/app/assets/javascripts/discourse/routes/preferences-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-index.js.es6 @@ -1,6 +1,8 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ + showFooter: true, + redirect() { this.transitionTo('preferences.account'); } diff --git a/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 b/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 index f58658956fc..729e79dedd9 100644 --- a/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-interface.js.es6 @@ -1,6 +1,8 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ + showFooter: true, + setupController(controller, user) { controller.setProperties({ model: user diff --git a/app/assets/javascripts/discourse/routes/preferences-notifications.js.es6 b/app/assets/javascripts/discourse/routes/preferences-notifications.js.es6 new file mode 100644 index 00000000000..713d79e4207 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-notifications.js.es6 @@ -0,0 +1,5 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + showFooter: true +}); diff --git a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 new file mode 100644 index 00000000000..b688ec813bc --- /dev/null +++ b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 @@ -0,0 +1,15 @@ +import RestrictedUserRoute from "discourse/routes/restricted-user"; + +export default RestrictedUserRoute.extend({ + model() { + return this.modelFor('user'); + }, + + renderTemplate() { + return this.render({ into: 'user' }); + }, + + setupController(controller, model) { + controller.setProperties({ model, newUsername: model.get('username') }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/preferences-username.js.es6 b/app/assets/javascripts/discourse/routes/preferences-username.js.es6 index 09fbc0ec6b0..6737345bcd0 100644 --- a/app/assets/javascripts/discourse/routes/preferences-username.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-username.js.es6 @@ -1,6 +1,8 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ + showFooter: true, + model: function() { return this.modelFor('user'); }, diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index e1cc50fc5cc..1eb3d4b3e17 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -3,6 +3,7 @@ import showModal from 'discourse/lib/show-modal'; import { popupAjaxError } from 'discourse/lib/ajax-error'; export default RestrictedUserRoute.extend({ + model() { return this.modelFor('user'); }, diff --git a/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6 b/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6 index 0d67542b2f9..b91bbf9aa07 100644 --- a/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/tag-groups-show.js.es6 @@ -1,4 +1,6 @@ export default Discourse.Route.extend({ + showFooter: true, + model(params) { return this.store.find('tagGroup', params.id); } diff --git a/app/assets/javascripts/discourse/routes/tag-groups.js.es6 b/app/assets/javascripts/discourse/routes/tag-groups.js.es6 index 6d6476964af..fb983936cf5 100644 --- a/app/assets/javascripts/discourse/routes/tag-groups.js.es6 +++ b/app/assets/javascripts/discourse/routes/tag-groups.js.es6 @@ -1,4 +1,6 @@ export default Discourse.Route.extend({ + showFooter: true, + model() { return this.store.findAll('tagGroup'); }, diff --git a/app/assets/javascripts/discourse/routes/tags-index.js.es6 b/app/assets/javascripts/discourse/routes/tags-index.js.es6 index dcbe6f19290..7b7bffedb9a 100644 --- a/app/assets/javascripts/discourse/routes/tags-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/tags-index.js.es6 @@ -1,6 +1,22 @@ +import Tag from "discourse/models/tag"; + export default Discourse.Route.extend({ model() { - return this.store.findAll('tag'); + return this.store.findAll("tag").then(result => { + if (result.extras) { + if (result.extras.categories) { + result.extras.categories.forEach(category => { + category.tags = category.tags.map(t => Tag.create(t)); + }); + } + if (result.extras.tag_groups) { + result.extras.tag_groups.forEach(tagGroup => { + tagGroup.tags = tagGroup.tags.map(t => Tag.create(t)); + }); + } + } + return result; + }); }, titleToken() { @@ -10,7 +26,7 @@ export default Discourse.Route.extend({ setupController(controller, model) { this.controllerFor('tags.index').setProperties({ model, - sortProperties: this.siteSettings.tags_sort_alphabetically ? ['id'] : ['count:desc', 'id'] + sortProperties: this.siteSettings.tags_sort_alphabetically ? ['id'] : ['totalCount:desc', 'id'] }); }, diff --git a/app/assets/javascripts/discourse/routes/tags-show.js.es6 b/app/assets/javascripts/discourse/routes/tags-show.js.es6 index eec3a6b3623..68e37e44eff 100644 --- a/app/assets/javascripts/discourse/routes/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/tags-show.js.es6 @@ -129,18 +129,22 @@ export default Discourse.Route.extend({ var controller = this.controllerFor("tags.show"), self = this; - this.controllerFor('composer').open({ - categoryId: controller.get('category.id'), - action: Composer.CREATE_TOPIC, - draftKey: controller.get('list.draft_key'), - draftSequence: controller.get('list.draft_sequence') - }).then(function() { - // Pre-fill the tags input field - if (controller.get('model.id')) { - var c = self.controllerFor('composer').get('model'); - c.set('tags', _.flatten([controller.get('model.id')], controller.get('additionalTags'))); - } - }); + if (controller.get('list.draft')) { + this.openTopicDraft(controller.get('list')); + } else { + this.controllerFor('composer').open({ + categoryId: controller.get('category.id'), + action: Composer.CREATE_TOPIC, + draftKey: controller.get('list.draft_key'), + draftSequence: controller.get('list.draft_sequence') + }).then(function() { + // Pre-fill the tags input field + if (controller.get('model.id')) { + var c = self.controllerFor('composer').get('model'); + c.set('tags', _.flatten([controller.get('model.id')], controller.get('additionalTags'))); + } + }); + } }, didTransition() { diff --git a/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 deleted file mode 100644 index 2faf69d0fb5..00000000000 --- a/app/assets/javascripts/discourse/routes/topic-unsubscribe.js.es6 +++ /dev/null @@ -1,19 +0,0 @@ -import { loadTopicView } from 'discourse/models/topic'; - -export default Discourse.Route.extend({ - model(params) { - const topic = this.store.createRecord("topic", { id: params.id }); - return loadTopicView(topic).then(() => topic); - }, - - afterModel(topic) { - topic.set("details.notificationReasonText", null); - }, - - actions: { - didTransition() { - this.controllerFor("application").set("showFooter", true); - return true; - } - } -}); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 9cc709d1979..281adb45ff5 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -85,6 +85,10 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor('modal').set('modalClass', 'history-modal'); }, + showGrantBadgeModal() { + showModal('grant-badge', { model: this.modelFor('topic'), title: 'admin.badges.grant_badge' }); + }, + showRawEmail(model) { showModal('raw-email', { model }); this.controllerFor('raw_email').loadRawEmail(model.get("id")); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-archive.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-archive.js.es6 index 7ebf279b290..ef304e9094b 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-archive.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-archive.js.es6 @@ -1,3 +1,3 @@ import createPMRoute from "discourse/routes/build-private-messages-route"; -export default createPMRoute('archive', 'private-messages-archive'); +export default createPMRoute('archive', 'private-messages-archive', 'archive'); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 index 19d84f9b7e1..0594b9d1128 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-group-archive.js.es6 @@ -1,26 +1,41 @@ import createPMRoute from "discourse/routes/build-private-messages-route"; export default createPMRoute('groups', 'private-messages-groups').extend({ - model(params) { - const username = this.modelFor("user").get("username_lower"); - return this.store.findFiltered("topicList", { - filter: `topics/private-messages-group/${username}/${params.name}/archive` - }); - }, + groupName: null, - afterModel(model) { - const split = model.get("filter").split('/'); - const groupName = split[split.length-2]; - const groups = this.modelFor("user").get("groups"); - const group = _.first(groups.filterBy("name", groupName)); - this.controllerFor("user-private-messages").set("group", group); - }, + titleToken() { + const groupName = this.get('groupName'); - setupController(controller, model) { - this._super.apply(this, arguments); - const split = model.get("filter").split('/'); - const group = split[split.length-2]; - this.controllerFor("user-private-messages").set("groupFilter", group); - this.controllerFor("user-private-messages").set("archive", true); - } + if (groupName) { + return [ + `${groupName.capitalize()} ${I18n.t('user.messages.archive')}`, + I18n.t("user.private_messages") + ]; + }; + }, + + model(params) { + const username = this.modelFor("user").get("username_lower"); + return this.store.findFiltered("topicList", { + filter: `topics/private-messages-group/${username}/${params.name}/archive` + }); + }, + + afterModel(model) { + const split = model.get("filter").split('/'); + const groupName = split[split.length-2]; + this.set("groupName", groupName); + const groups = this.modelFor("user").get("groups"); + const group = _.first(groups.filterBy("name", groupName)); + this.controllerFor("user-private-messages").set("group", group); + }, + + setupController(controller, model) { + this._super.apply(this, arguments); + const split = model.get("filter").split('/'); + const group = split[split.length-2]; + this.controllerFor("user-private-messages").set("groupFilter", group); + this.controllerFor("user-private-messages").set("archive", true); + this.controllerFor("user-topics-list").subscribe(`/private-messages/group/${group}/archive`); + } }); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6 index 0b7f923f208..556f65fded1 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-group.js.es6 @@ -1,24 +1,33 @@ import createPMRoute from "discourse/routes/build-private-messages-route"; export default createPMRoute('groups', 'private-messages-groups').extend({ - model(params) { - const username = this.modelFor("user").get("username_lower"); - return this.store.findFiltered("topicList", { - filter: `topics/private-messages-group/${username}/${params.name}` - }); - }, + groupName: null, - afterModel(model) { - const groupName = _.last(model.get("filter").split('/')); - const groups = this.modelFor("user").get("groups"); - const group = _.first(groups.filterBy("name", groupName)); - this.controllerFor("user-private-messages").set("group", group); - }, + titleToken() { + const groupName = this.get('groupName'); + if (groupName) return [groupName.capitalize(), I18n.t("user.private_messages")]; + }, - setupController(controller, model) { - this._super.apply(this, arguments); - const group = _.last(model.get("filter").split('/')); - this.controllerFor("user-private-messages").set("groupFilter", group); - this.controllerFor("user-private-messages").set("archive", false); - } + model(params) { + const username = this.modelFor("user").get("username_lower"); + return this.store.findFiltered("topicList", { + filter: `topics/private-messages-group/${username}/${params.name}` + }); + }, + + afterModel(model) { + const groupName = _.last(model.get("filter").split('/')); + this.set("groupName", groupName); + const groups = this.modelFor("user").get("groups"); + const group = _.first(groups.filterBy("name", groupName)); + this.controllerFor("user-private-messages").set("group", group); + }, + + setupController(controller, model) { + this._super.apply(this, arguments); + const group = _.last(model.get("filter").split('/')); + this.controllerFor("user-private-messages").set("groupFilter", group); + this.controllerFor("user-private-messages").set("archive", false); + this.controllerFor("user-topics-list").subscribe(`/private-messages/group/${group}`); + } }); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6 index d1e2bf3c275..127d64cfee6 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-index.js.es6 @@ -1,3 +1,3 @@ import createPMRoute from "discourse/routes/build-private-messages-route"; -export default createPMRoute('index', 'private-messages'); +export default createPMRoute('index', 'private-messages', 'inbox'); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-sent.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-sent.js.es6 index b31c2f4c9fe..2312be91796 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-sent.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-sent.js.es6 @@ -1,3 +1,3 @@ import createPMRoute from "discourse/routes/build-private-messages-route"; -export default createPMRoute('sent', 'private-messages-sent'); +export default createPMRoute('sent', 'private-messages-sent', 'sent'); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 new file mode 100644 index 00000000000..3ab2439e86e --- /dev/null +++ b/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 @@ -0,0 +1,11 @@ +import createPMRoute from "discourse/routes/build-private-messages-route"; + +export default createPMRoute('tags', 'private-messages-tags').extend({ + model(params) { + this.controllerFor('user-private-messages').set('tagId', params.id); + const username = this.modelFor("user").get("username_lower"); + return this.store.findFiltered("topicList", { + filter: `topics/private-messages-tags/${username}/${params.id}` + }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6 new file mode 100644 index 00000000000..d2297f23842 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/user-private-messages-tags.js.es6 @@ -0,0 +1,27 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Discourse.Route.extend({ + model() { + const username = this.modelFor("user").get("username_lower"); + return ajax(`/tags/personal_messages/${username}`).then(result => { + return result.tags.map(tag => Ember.Object.create(tag)); + }).catch(popupAjaxError); + }, + + titleToken() { + return [I18n.t("tagging.tags"), I18n.t("user.private_messages")]; + }, + + setupController(controller, model) { + this.controllerFor('user-private-messages-tags').setProperties({ + model, + sortProperties: this.siteSettings.tags_sort_alphabetically ? ['id'] : ['count:desc', 'id'], + tagsForUser: this.modelFor("user").get("username_lower") + }); + this.controllerFor('user-private-messages').setProperties({ + showToggleBulkSelect: false, + pmView: "tags" + }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/user-summary.js.es6 b/app/assets/javascripts/discourse/routes/user-summary.js.es6 index a191c418722..39d4571973d 100644 --- a/app/assets/javascripts/discourse/routes/user-summary.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-summary.js.es6 @@ -1,4 +1,6 @@ export default Discourse.Route.extend({ + showFooter: true, + model() { return this.modelFor("user").summary(); }, diff --git a/app/assets/javascripts/discourse/routes/users.js.es6 b/app/assets/javascripts/discourse/routes/users.js.es6 index ab3cbe32060..9cb825c0f47 100644 --- a/app/assets/javascripts/discourse/routes/users.js.es6 +++ b/app/assets/javascripts/discourse/routes/users.js.es6 @@ -3,7 +3,9 @@ export default Discourse.Route.extend({ period: { refreshModel: true }, order: { refreshModel: true }, asc: { refreshModel: true }, - name: { refreshModel: true, replace: true } + name: { refreshModel: true, replace: true }, + group: { refreshModel: true }, + exclude_usernames: { refreshModel: true } }, refreshQueryWithoutTransition: true, @@ -18,7 +20,9 @@ export default Discourse.Route.extend({ period: "weekly", order: "likes_received", asc: null, - name: "" + name: "", + group: null, + exclude_usernames: null }); } }, diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs index dfe8853dcec..c75d0087c1f 100644 --- a/app/assets/javascripts/discourse/templates/application.hbs +++ b/app/assets/javascripts/discourse/templates/application.hbs @@ -14,6 +14,7 @@ {{#if showTop}} {{custom-html name="top"}} {{/if}} + {{notification-consent-banner}} {{global-notice}} {{create-topics-notice}}
      @@ -23,7 +24,7 @@ {{plugin-outlet name="above-footer" args=(hash showFooter=showFooter)}} {{#if showFooter}} - {{custom-html name="footer"}} + {{custom-html name="footer" triggerAppEvent="true"}} {{/if}} {{plugin-outlet name="below-footer" args=(hash showFooter=showFooter)}} diff --git a/app/assets/javascripts/discourse/templates/badge-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/badge-selector-autocomplete.raw.hbs index 4537c451601..611ba05f3a9 100644 --- a/app/assets/javascripts/discourse/templates/badge-selector-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/badge-selector-autocomplete.raw.hbs @@ -1,4 +1,4 @@ -
      +
        {{#each options as |option|}}
      • {{option.name}}
      • diff --git a/app/assets/javascripts/discourse/templates/badges/index.hbs b/app/assets/javascripts/discourse/templates/badges/index.hbs index 12adeb3d06f..b0e2c030f96 100644 --- a/app/assets/javascripts/discourse/templates/badges/index.hbs +++ b/app/assets/javascripts/discourse/templates/badges/index.hbs @@ -8,10 +8,11 @@

        {{bg.badgeGrouping.displayName}}

        - - {{#each bg.badges as |b|}} - {{badge-card badge=b filterUser=b.has_badge username=currentUser.username}} - {{/each}} +
        + {{#each bg.badges as |b|}} + {{badge-card badge=b filterUser=b.has_badge username=currentUser.username}} + {{/each}} +
      {{/each}}
      diff --git a/app/assets/javascripts/discourse/templates/badges/show.hbs b/app/assets/javascripts/discourse/templates/badges/show.hbs index fab3e9a5aad..952bac8b4f8 100644 --- a/app/assets/javascripts/discourse/templates/badges/show.hbs +++ b/app/assets/javascripts/discourse/templates/badges/show.hbs @@ -37,6 +37,7 @@ {{#if userBadges}}
      {{#load-more selector=".badge-info" action="loadMore"}} +
      {{#each userBadges as |ub|}} {{#user-info user=ub.user size="medium" class="badge-info" date=ub.granted_at}}
      {{i18n 'badges.granted_on' date=(inline-date ub.granted_at)}}
      @@ -45,6 +46,7 @@ {{/if}} {{/user-info}} {{/each}} +
      {{/load-more}} {{#unless canLoadMore}} diff --git a/app/assets/javascripts/discourse/templates/bulk-tag.hbs b/app/assets/javascripts/discourse/templates/bulk-tag.hbs index 4ed48ddc2b6..3a54edf758d 100644 --- a/app/assets/javascripts/discourse/templates/bulk-tag.hbs +++ b/app/assets/javascripts/discourse/templates/bulk-tag.hbs @@ -1,5 +1,5 @@

      {{i18n (concat "topics.bulk." title)}}

      -

      {{tag-chooser tags=tags categoryId=categoryId}}

      +

      {{tag-chooser filterPlaceholder=null tags=tags categoryId=categoryId}}

      {{d-button action=action disabled=emptyTags label=(concat "topics.bulk." label)}} diff --git a/app/assets/javascripts/discourse/templates/category-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-selector-autocomplete.raw.hbs index bc2181f534a..76aad2442f5 100644 --- a/app/assets/javascripts/discourse/templates/category-selector-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/category-selector-autocomplete.raw.hbs @@ -1,4 +1,4 @@ -
      +
        {{#each options as |option|}}
      • {{category-link option allowUncategorized="true" link="false"}}
      • diff --git a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs index 3f4359aab56..7d1d72e88df 100644 --- a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs @@ -1,4 +1,4 @@ -
        +
          {{#each options as |option|}}
        • diff --git a/app/assets/javascripts/discourse/templates/components/badge-button.hbs b/app/assets/javascripts/discourse/templates/components/badge-button.hbs index 8116e5195be..4dd20f5cdb0 100644 --- a/app/assets/javascripts/discourse/templates/components/badge-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/badge-button.hbs @@ -1,3 +1,3 @@ -{{icon-or-image badge.icon}} +{{icon-or-image badge}} {{badge.name}} {{yield}} diff --git a/app/assets/javascripts/discourse/templates/components/badge-card.hbs b/app/assets/javascripts/discourse/templates/components/badge-card.hbs index 3c519d85ddf..4d30b969767 100644 --- a/app/assets/javascripts/discourse/templates/components/badge-card.hbs +++ b/app/assets/javascripts/discourse/templates/components/badge-card.hbs @@ -5,13 +5,15 @@ {{d-icon "check"}} {{/if}}
          -
          - {{icon-or-image badge.icon}} -
          - diff --git a/app/assets/javascripts/discourse/templates/components/badge-title.hbs b/app/assets/javascripts/discourse/templates/components/badge-title.hbs index 8101829c4d1..0ac803fdad8 100644 --- a/app/assets/javascripts/discourse/templates/components/badge-title.hbs +++ b/app/assets/javascripts/discourse/templates/components/badge-title.hbs @@ -10,7 +10,10 @@
          - {{combo-box value=selectedUserBadgeId nameProperty="badge.name" content=selectableUserBadges}} + {{combo-box + value=selectedUserBadgeId + nameProperty="badge.name" + content=selectableUserBadges}}
          diff --git a/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs index c174d4a9b42..a15dc6276db 100644 --- a/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs +++ b/app/assets/javascripts/discourse/templates/components/basic-topic-list.hbs @@ -1,4 +1,12 @@ {{#conditional-loading-spinner condition=loading}} + {{#if hasIncoming}} +
          +
          + {{count-i18n key="topic_count_" suffix="latest" count=incomingCount}} +
          +
          + {{/if}} + {{#if topics}} {{topic-list showParticipants=showParticipants showPosters=showPosters @@ -8,7 +16,8 @@ bulkSelectEnabled=bulkSelectEnabled canBulkSelect=canBulkSelect selected=selected - skipHeader=skipHeader}} + skipHeader=skipHeader + tagsForUser=tagsForUser}} {{else}} {{#unless loadingMore}}
          diff --git a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs index bbf8ef4caba..e46d6c32e15 100644 --- a/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs +++ b/app/assets/javascripts/discourse/templates/components/bread-crumbs.hbs @@ -1,7 +1,14 @@ -{{category-drop category=firstCategory categories=parentCategories}} +{{category-drop + category=firstCategory + categories=parentCategories}} {{#if childCategories}} - {{category-drop category=secondCategory parentCategory=firstCategory categories=childCategories subCategory="true" noSubcategories=noSubcategories}} + {{category-drop + category=secondCategory + parentCategory=firstCategory + categories=childCategories + subCategory=true + noSubcategories=noSubcategories}} {{/if}} {{#if siteSettings.tagging_enabled}} diff --git a/app/assets/javascripts/discourse/templates/components/cancel-link.hbs b/app/assets/javascripts/discourse/templates/components/cancel-link.hbs new file mode 100644 index 00000000000..8c3a3cb4c7e --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/cancel-link.hbs @@ -0,0 +1,3 @@ +{{#link-to route args}} + {{i18n 'cancel'}} +{{/link-to}} diff --git a/app/assets/javascripts/discourse/templates/components/categories-and-latest-topics.hbs b/app/assets/javascripts/discourse/templates/components/categories-and-latest-topics.hbs index 1a32cea92d3..be7ee3e4a31 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-and-latest-topics.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-and-latest-topics.hbs @@ -3,5 +3,5 @@
          - {{latest-topic-list topics=topics}} + {{categories-topic-list topics=topics filter="latest" class="latest-topic-list"}}
          diff --git a/app/assets/javascripts/discourse/templates/components/categories-and-top-topics.hbs b/app/assets/javascripts/discourse/templates/components/categories-and-top-topics.hbs new file mode 100644 index 00000000000..4e84c4d6318 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/categories-and-top-topics.hbs @@ -0,0 +1,7 @@ +
          + {{categories-only categories=categories}} +
          + +
          + {{categories-topic-list topics=topics filter="top" class="top-topic-list"}} +
          diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs index 434e18fe629..aae157c5213 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-boxes-with-topics.hbs @@ -11,6 +11,7 @@ {{#if c.read_restricted}} {{d-icon 'lock'}} {{/if}} + {{category-title-before category=c}} {{c.name}} diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs index c473668648f..0c90a54a03f 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs @@ -1,24 +1,22 @@ {{#each categories as |c|}} -
          + {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/categories-only.hbs b/app/assets/javascripts/discourse/templates/components/categories-only.hbs index f2db6778627..282e5d520cc 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-only.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-only.hbs @@ -2,21 +2,21 @@ - + {{#if showTopics}} {{/if}} - + {{#each categories as |c|}} + - diff --git a/app/assets/javascripts/discourse/templates/components/group-manage-save-button.hbs b/app/assets/javascripts/discourse/templates/components/group-manage-save-button.hbs new file mode 100644 index 00000000000..0276ac16e5d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-manage-save-button.hbs @@ -0,0 +1,12 @@ +
          + {{#d-button action="save" + disabled=saving + class='btn btn-primary group-manage-save'}} + + {{savingText}} + {{/d-button}} + + {{#if saved}} + {{i18n 'saved'}} + {{/if}} +
          diff --git a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs index ad66499dbbd..036bf9b1ca9 100644 --- a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs @@ -1,4 +1,4 @@ - + {{#if model.members}}
          @@ -16,7 +16,7 @@ {{#unless model.automatic}}
          {{user-selector usernames=model.usernames - placeholderKey="admin.groups.selector_placeholder" + placeholderKey="groups.selector_placeholder" id="member-selector"}} {{#if addButton}} @@ -24,7 +24,7 @@ class="add" icon="plus" disabled=disableAddButton - label="groups.edit.add_members"}} + label="groups.manage.add_members"}} {{/if}}
          {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs b/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs index 8240f372f93..2bd19564927 100644 --- a/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-membership-button.hbs @@ -11,25 +11,9 @@ label="groups.leave" disabled=updatingMembership}} {{else if model.allow_membership_requests}} - {{#if userIsGroupUser}} - {{#if showMembershipStatus}} - {{d-button - class="btn-primary" - icon="user" - label="groups.is_group_user" - disabled=true}} - {{/if}} - {{else}} - {{d-button action="showRequestMembershipForm" - class="group-index-request" - disabled=loading - icon="user-plus" - label="groups.request"}} - - {{#if loading}} - {{loading-spinner size="small"}} - {{/if}} - {{/if}} -{{else}} - {{yield}} + {{d-button action="showRequestMembershipForm" + class="group-index-request" + disabled=loading + icon="user-plus" + label="groups.request"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs index 8501e9e2f54..29ad6b7f1e1 100644 --- a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs @@ -1,5 +1,15 @@ {{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=currentPath}} - {{#each availableTabs as |tab|}} + {{#if site.mobileView}} +
        • + {{#link-to "groups.index"}} + {{i18n "groups.index.all"}} + {{/link-to}} +
        • + {{else}} + {{group-dropdown content=group.extras.visible_group_names value=group.name}} + {{/if}} + + {{#each tabs as |tab|}}
        • {{#link-to tab.route group title=tab.message class=tab.name}} {{#if tab.icon}}{{d-icon tab.icon}}{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index 9f51a1584f0..9e56010fad0 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -1,13 +1,15 @@ -
          -
          + - -
          + +
          {{#if post.expandedExcerpt}} {{{post.expandedExcerpt}}} {{else}} diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs new file mode 100644 index 00000000000..5ed87a5c027 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-form-interaction-fields.hbs @@ -0,0 +1,58 @@ +{{#if currentUser.admin}} +
          + + + + {{combo-box name="alias" + valueAttribute="value" + value=model.visibility_level + content=visibilityLevelOptions + castInteger=true + class="groups-form-visibility-level"}} +
          +{{/if}} + +
          + + + + {{combo-box name="alias" + valueAttribute="value" + value=model.mentionable_level + content=aliasLevelOptions + class="groups-form-mentionable-level"}} +
          + +
          + + + {{combo-box name="alias" + valueAttribute="value" + value=model.messageable_level + content=aliasLevelOptions + class="groups-form-messageable-level"}} +
          + +{{#if showEmailSettings}} +
          + + + + {{text-field name="incoming_email" + class="input-xxlarge groups-form-incoming-email" + value=model.incoming_email + placeholderKey="admin.groups.manage.interaction.incoming_email_placeholder"}} + + {{plugin-outlet name="group-email-in" args=(hash model=model)}} +
          +{{/if}} + + + +
          + + + {{notifications-button i18nPrefix='groups.notifications' + value=model.default_notification_level + class="groups-form-default-notification-level"}} +
          diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs new file mode 100644 index 00000000000..03f5755d8f9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-form-membership-fields.hbs @@ -0,0 +1,82 @@ +{{#if currentUser.admin}} +
          + + + + + {{list-setting name="automatic_membership" + settingValue=model.emailDomains + class="group-form-automatic-membership-automatic"}} + + + + +
          + +
          + + + + {{combo-box name="grant_trust_level" + valueAttribute="value" + value=model.grant_trust_level + content=trustLevelOptions + class="groups-form-grant-trust-level"}} +
          +{{/if}} + +
          + + + + + + + + + {{#if model.allow_membership_requests}} +
          + + + {{expanding-text-area name="membership-request-template" + class='group-form-membership-request-template input-xxlarge' + value=model.membership_request_template}} +
          + {{/if}} +
          diff --git a/app/assets/javascripts/discourse/templates/components/groups-form-profile-fields.hbs b/app/assets/javascripts/discourse/templates/components/groups-form-profile-fields.hbs new file mode 100644 index 00000000000..81de1132251 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-form-profile-fields.hbs @@ -0,0 +1,47 @@ +{{#if this.currentUser.admin}} +
          + + + {{text-field name="name" + class="input-xxlarge group-form-name" + value=nameInput + placeholderKey="admin.groups.name_placeholder"}} + + {{input-tip validation=nameValidation}} +
          +{{/if}} + +
          + + + {{text-field name='full_name' + class="input-xxlarge group-form-full-name" + value=model.full_name}} +
          + +{{#if this.currentUser.admin}} +
          + + + {{input value=model.title name="title" class="input-xxlarge"}} + +
          + {{i18n 'admin.groups.default_title_description'}} +
          +
          +{{/if}} + +
          + + {{d-editor value=model.bio_raw class="group-form-bio input-xxlarge"}} +
          + +{{yield}} + +
          + {{group-flair-inputs model=model}} +
          + +{{plugin-outlet name="group-edit" args=(hash group=model)}} diff --git a/app/assets/javascripts/discourse/templates/components/groups-info.hbs b/app/assets/javascripts/discourse/templates/components/groups-info.hbs new file mode 100644 index 00000000000..bf1628996e1 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/groups-info.hbs @@ -0,0 +1,11 @@ +{{group.displayName}} + +{{#if showFullName}} + {{group.full_name}} +{{/if}} + +{{#if group.title}} +
          + {{group.title}} +
          +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/latest-topic-list-contents.hbs b/app/assets/javascripts/discourse/templates/components/latest-topic-list-contents.hbs deleted file mode 100644 index 33b8ec95b90..00000000000 --- a/app/assets/javascripts/discourse/templates/components/latest-topic-list-contents.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#each topics as |t|}} - {{latest-topic-list-item topic=t}} -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/latest-topic-list.hbs b/app/assets/javascripts/discourse/templates/components/latest-topic-list.hbs deleted file mode 100644 index 2390a154ab4..00000000000 --- a/app/assets/javascripts/discourse/templates/components/latest-topic-list.hbs +++ /dev/null @@ -1,14 +0,0 @@ -
          - {{i18n "filters.latest.title"}} -
          - -{{#if topics}} - {{latest-topic-list-contents topics=topics tagName=""}} - -{{else}} -
          -

          {{i18n "topics.none.latest"}}

          -
          -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/login-buttons.hbs b/app/assets/javascripts/discourse/templates/components/login-buttons.hbs index 776ddff179e..bdd430c4e7a 100644 --- a/app/assets/javascripts/discourse/templates/components/login-buttons.hbs +++ b/app/assets/javascripts/discourse/templates/components/login-buttons.hbs @@ -1,3 +1,12 @@ {{#each buttons as |b|}} {{/each}} + +{{#if canLoginLocalWithEmail}} + {{d-button + action="emailLogin" + label="email_login.button_label" + disabled=processingEmailLink + icon="envelope-o" + class="login-with-email-button"}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/notification-consent-banner.hbs b/app/assets/javascripts/discourse/templates/components/notification-consent-banner.hbs new file mode 100644 index 00000000000..f258abf113e --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/notification-consent-banner.hbs @@ -0,0 +1,8 @@ +{{#if showNotificationPromptBanner}} +
          + +
          +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/period-chooser.hbs b/app/assets/javascripts/discourse/templates/components/period-chooser.hbs deleted file mode 100644 index ce4d6066c57..00000000000 --- a/app/assets/javascripts/discourse/templates/components/period-chooser.hbs +++ /dev/null @@ -1,11 +0,0 @@ -

          {{period-title period showDateRange=true}}

          - - -
          - -
          -
          diff --git a/app/assets/javascripts/discourse/templates/components/queued-post.hbs b/app/assets/javascripts/discourse/templates/components/queued-post.hbs index de2709ca4ab..99162937d14 100644 --- a/app/assets/javascripts/discourse/templates/components/queued-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/queued-post.hbs @@ -52,7 +52,7 @@ {{/each}}
          {{else if editTags}} - {{tag-chooser tags=buffered.tags categoryId=buffered.category_id width='100%'}} + {{tag-chooser tags=buffered.tags categoryId=buffered.category_id}} {{/if}}
          diff --git a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs index f53a0e0c50b..371a1c3849a 100644 --- a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs @@ -40,7 +40,12 @@
          - {{tag-chooser tags=searchedTerms.tags blacklist=searchedTerms.tags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true" width="70%"}} + {{tag-chooser + tags=searchedTerms.tags + allowCreate=false + filterPlaceholder=null + everyTag=true + unlimitedTagCount=true}}
          @@ -55,6 +60,7 @@
          {{#if currentUser}}
          + diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs new file mode 100644 index 00000000000..c6e6e6d4355 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/second-factor-form.hbs @@ -0,0 +1,13 @@ +
        • {{i18n 'categories.category'}}{{i18n 'categories.category'}} {{i18n 'categories.topics'}}{{i18n 'categories.latest'}}
          {{category-title-link category=c}}
          - {{{c.description_excerpt}}} + {{{dir-span c.description_excerpt}}}
          @@ -24,6 +24,7 @@
          {{#each c.subcategories as |s|}} + {{category-title-before category=s}} {{category-link s hideParent="true"}} {{category-unread category=s}} diff --git a/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs b/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs new file mode 100644 index 00000000000..7761050a38a --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs @@ -0,0 +1,16 @@ +
          + {{i18n (concat "filters." filter ".title")}} +
          + +{{#if topics}} + {{#each topics as |t|}} + {{latest-topic-list-item topic=t}} + {{/each}} + +{{else}} +
          +

          {{i18n (concat "topics.none." filter)}}

          +
          +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/category-drop.hbs b/app/assets/javascripts/discourse/templates/components/category-drop.hbs deleted file mode 100644 index ba5062a429b..00000000000 --- a/app/assets/javascripts/discourse/templates/components/category-drop.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{#if category}} - - - {{#if category.read_restricted}} - {{d-icon "lock"}} - {{/if}} - {{category.name}} - -{{else}} - {{#if noSubcategories}} - {{i18n 'categories.no_subcategory'}} - {{else}} - {{allCategoriesLabel}} - {{/if}} -{{/if}} - -{{#if categories}} - - {{d-icon expandIcon}} - -
          - - {{#if subCategory}} - - {{/if}} - {{#if renderCategories}} - {{#each categories as |c|}}
          {{category-link c allowUncategorized=true hideParent=subCategory}}
          {{/each}} - {{/if}} -
          -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/category-name-fields.hbs b/app/assets/javascripts/discourse/templates/components/category-name-fields.hbs new file mode 100644 index 00000000000..e0766f48284 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/category-name-fields.hbs @@ -0,0 +1,10 @@ +
          +
          + + {{text-field value=category.name placeholderKey="category.name_placeholder" maxlength="50"}} +
          +
          + + {{text-field value=category.slug placeholderKey="category.slug_placeholder" maxlength="255"}} +
          +
          diff --git a/app/assets/javascripts/discourse/templates/components/category-title-before.hbs b/app/assets/javascripts/discourse/templates/components/category-title-before.hbs new file mode 100644 index 00000000000..af92a5ecd6f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/category-title-before.hbs @@ -0,0 +1 @@ +{{plugin-outlet name="category-title-before" noTags=true args=(hash category=category)}} diff --git a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs index fcb0bdeb071..f27eac550c9 100644 --- a/app/assets/javascripts/discourse/templates/components/category-title-link.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-title-link.hbs @@ -1,10 +1,12 @@ - - {{#if category.read_restricted}} - {{d-icon 'lock'}} - {{/if}} - - {{category.name}} +{{category-title-before category=category}} + +
          + {{#if category.read_restricted}} + {{d-icon 'lock'}} + {{/if}} + {{dir-span category.name}} +
          {{#if category.uploaded_logo.url}}
          {{cdn-img src=category.uploaded_logo.url class="category-logo"}}
          {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs b/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs new file mode 100644 index 00000000000..0019b1d1797 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/composer-action-title.hbs @@ -0,0 +1,14 @@ +{{#if isEditing}} + {{d-icon "pencil"}} +{{else}} + {{composer-actions + composerModel=model + options=model.replyOptions + canWhisper=canWhisper + action=model.action + tabindex=tabindex}} +{{/if}} + + + {{actionTitle}} + diff --git a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs index 006cd5eddc8..ebf9a3d856f 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs @@ -1,20 +1,24 @@ -{{d-editor tabindex="4" - value=composer.reply - placeholder="composer.reply_placeholder" - previewUpdated="previewUpdated" - markdownOptions=markdownOptions - extraButtons="extraButtons" - importQuote="importQuote" - showUploadModal="showUploadModal" - togglePreview="togglePreview" - validation=validation - loading=composer.loading - forcePreview=forcePreview - composerEvents=true - onExpandPopupMenuOptions="onExpandPopupMenuOptions" - onPopupMenuAction=onPopupMenuAction - popupMenuOptions=popupMenuOptions}} +{{d-editor + tabindex="4" + value=composer.reply + placeholder=replyPlaceholder + previewUpdated="previewUpdated" + markdownOptions=markdownOptions + extraButtons="extraButtons" + importQuote="importQuote" + showUploadModal="showUploadModal" + togglePreview="togglePreview" + validation=validation + loading=composer.loading + forcePreview=forcePreview + showLink=currentUser.can_post_link + composerEvents=true + onExpandPopupMenuOptions="onExpandPopupMenuOptions" + onPopupMenuAction=onPopupMenuAction + popupMenuOptions=popupMenuOptions + disabled=disableTextarea + outletArgs=(hash composer=composer editorType="composer")}} {{#if site.mobileView}} - + {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs index 99fc0a5b97d..0722426f163 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-user-selector.hbs @@ -3,7 +3,6 @@ onChangeCallback='triggerResize' id="private-message-users" includeMessageableGroups='true' - class="span8" placeholderKey="composer.users_placeholder" tabindex="1" usernames=usernames diff --git a/app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs b/app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs new file mode 100644 index 00000000000..b97ce25a01f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/conditional-loading-section.hbs @@ -0,0 +1,6 @@ +{{#if isLoading}} + {{title}} +
          +{{else}} + {{yield}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs b/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs index ee1172ffe23..e99673f9cb4 100644 --- a/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/create-topic-button.hbs @@ -5,5 +5,5 @@ action=action icon="plus" disabled=disabled - label="topic.create"}} + label=label}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 6133b28f8fc..be1f2d50dad 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -9,7 +9,7 @@
          -

          {{title}}

          +
          +

          {{title}}

          + + {{#if subtitle}} +

          {{subtitle}}

          + {{/if}} +
          diff --git a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs index 6f8c71f5249..2d1b956b610 100644 --- a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs @@ -15,7 +15,9 @@ {{create-topic-button canCreateTopic=canCreateTopic action=createTopic - disabled=createTopicDisabled}} + disabled=createTopicDisabled + label=createTopicLabel +}} {{#if showCategoryEdit}} {{d-button diff --git a/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs b/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs index dc8edd2018c..d1ffffbdee7 100644 --- a/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs +++ b/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs @@ -1,15 +1,10 @@ - {{#if isNotSupported}} {{d-button icon="bell-slash" label="user.desktop_notifications.not_supported" disabled="true"}} {{/if}} -{{#if isDefaultPermission}} - {{d-button icon="bell-slash" label="user.desktop_notifications.perm_default" action="requestPermission"}} -{{/if}} {{#if isDeniedPermission}} {{d-button icon="bell-slash" label="user.desktop_notifications.perm_denied_btn" action="recheckPermission" disabled='true'}} {{i18n "user.desktop_notifications.perm_denied_expl"}} -{{/if}} -{{#if isGrantedPermission}} +{{else}} {{#if isEnabled}} {{d-button icon="bell-slash-o" label="user.desktop_notifications.disable" action="turnoff"}} {{i18n "user.desktop_notifications.currently_enabled"}} diff --git a/app/assets/javascripts/discourse/templates/components/discourse-linked-text.hbs b/app/assets/javascripts/discourse/templates/components/discourse-linked-text.hbs new file mode 100644 index 00000000000..dd15a6dc6a4 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/discourse-linked-text.hbs @@ -0,0 +1 @@ +{{{translatedText}}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs index c31406819a7..2e498f77c3f 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs @@ -1,14 +1,5 @@
          -
          -
          - - {{text-field value=category.name placeholderKey="category.name_placeholder" maxlength="50"}} -
          -
          - - {{text-field value=category.slug placeholderKey="category.slug_placeholder" maxlength="255"}} -
          -
          + {{category-name-fields category=category tagName=""}} {{#if canSelectParentCategory}}
          diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index fa7b2a5653e..36e6bfc0b4c 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -21,8 +21,8 @@
          @@ -69,7 +69,7 @@
          {{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}} {{#unless isDefaultSortOrder}} - {{combo-box valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}} + {{combo-box castBoolean=true valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}} {{/unless}}
          @@ -152,4 +152,13 @@ {{/unless}} +{{#if siteSettings.tagging_enabled}} +
          + +
          +{{/if}} + {{plugin-outlet name="category-custom-settings" args=(hash category=category)}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-tags.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-tags.hbs index df6544d770b..c8ea0536360 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-tags.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-tags.hbs @@ -1,7 +1,11 @@

          {{i18n 'category.tags_allowed_tags'}}

          - {{tag-chooser placeholderKey="category.tags_placeholder" tags=category.allowed_tags everyTag="true" unlimitedTagCount="true"}} + {{tag-chooser + filterPlaceholder="category.tags_placeholder" + tags=category.allowed_tags + everyTag=true + unlimitedTagCount=true}}

          {{i18n 'category.tags_allowed_tag_groups'}}

          - {{tag-group-chooser placeholderKey="category.tag_groups_placeholder" tagGroups=category.allowed_tag_groups}} + {{tag-group-chooser tagGroups=category.allowed_tag_groups}}
          diff --git a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs index f90534931b7..52f618fec96 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs @@ -9,7 +9,7 @@ input=topicTimer.updateTime statusType=selection includeWeekend=true - basedOnLastPost=false}} + basedOnLastPost=topicTimer.based_on_last_post}} {{else if publishToCategory}}
          @@ -23,7 +23,7 @@ statusType=selection includeWeekend=true categoryId=topicTimer.category_id - basedOnLastPost=false}} + basedOnLastPost=topicTimer.based_on_last_post}} {{else if autoClose}} {{future-date-input input=topicTimer.updateTime diff --git a/app/assets/javascripts/discourse/templates/components/group-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/group-card-contents.hbs new file mode 100644 index 00000000000..b458965cec3 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-card-contents.hbs @@ -0,0 +1,72 @@ +{{#if visible}} +
          + + +
          + +

          + {{ group.name }} +

          + {{#if group.full_name}} +

          {{group.full_name}}

          + {{else}} +

          {{group.name}}

          + {{/if}} +
          +
          + +
          + {{group-membership-button + model=group + showLogin='showLogin'}} + + {{#if group.messageable}} + {{d-button + action="messageGroup" + class="btn-primary group-message-button inline" + icon="envelope" + label="groups.message"}} + {{/if}} +
          + + {{#if group.bio_cooked}}
          {{{group.bio_cooked}}}
          {{/if}} + + + {{#if group.members}} + + {{/if}} +
          +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/group-flair-inputs.hbs b/app/assets/javascripts/discourse/templates/components/group-flair-inputs.hbs index 5693422c4d5..ad0530ba8e7 100644 --- a/app/assets/javascripts/discourse/templates/components/group-flair-inputs.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-flair-inputs.hbs @@ -1,36 +1,35 @@ -
          -
          - - {{text-field name="flair_url" - value=model.flair_url - placeholderKey="groups.flair_url_placeholder"}} -
          +
          + -
          - - {{text-field name="flair_bg_color" - class="group-flair-bg-color" - value=model.flair_bg_color - placeholderKey="groups.flair_bg_color_placeholder"}} -
          - - {{#if flairPreviewIcon}} -
          - - {{text-field name="flair_color" - class="group-flair-color" - value=model.flair_color - placeholderKey="groups.flair_color_placeholder"}} -
          - {{/if}} - -
          - {{i18n 'groups.flair_note'}} -
          + {{text-field name="flair_url" + class="input-xxlarge" + value=model.flair_url + placeholderKey="groups.flair_url_placeholder"}}
          -
          - +
          + + + {{text-field name="flair_bg_color" + class="group-flair-bg-color input-xxlarge" + value=model.flair_bg_color + placeholderKey="groups.flair_bg_color_placeholder"}} +
          + +{{#if flairPreviewIcon}} +
          + + + {{text-field name="flair_color" + class="group-flair-color input-xxlarge" + value=model.flair_color + placeholderKey="groups.flair_color_placeholder"}} +
          +{{/if}} + +
          + +
          diff --git a/app/assets/javascripts/discourse/templates/components/group-logs-filter.hbs b/app/assets/javascripts/discourse/templates/components/group-manage-logs-filter.hbs similarity index 53% rename from app/assets/javascripts/discourse/templates/components/group-logs-filter.hbs rename to app/assets/javascripts/discourse/templates/components/group-manage-logs-filter.hbs index f9f13474c7f..1625fa5fc92 100644 --- a/app/assets/javascripts/discourse/templates/components/group-logs-filter.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-manage-logs-filter.hbs @@ -1,5 +1,5 @@ {{#if value}} - {{#d-button class="btn-small group-logs-filter" action="clearFilter" actionParam=type}} + {{#d-button class="btn-small group-manage-logs-filter" action="clearFilter" actionParam=type}} {{label}}: {{filterText}} {{d-icon "times-circle"}} {{/d-button}} diff --git a/app/assets/javascripts/discourse/templates/components/group-logs-row.hbs b/app/assets/javascripts/discourse/templates/components/group-manage-logs-row.hbs similarity index 80% rename from app/assets/javascripts/discourse/templates/components/group-logs-row.hbs rename to app/assets/javascripts/discourse/templates/components/group-manage-logs-row.hbs index 6fe9bfd858a..da955686e19 100644 --- a/app/assets/javascripts/discourse/templates/components/group-logs-row.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-manage-logs-row.hbs @@ -1,4 +1,4 @@ -
          {{#d-button class="btn-small" action="filter" actionParam=(hash value=log.action key="action")}} {{log.actionTitle}} @@ -33,7 +33,7 @@ {{bound-date log.created_at}} + {{#if log.prev_value}} {{#if expandDetails}} {{d-icon 'ellipsis-v'}} @@ -48,11 +48,11 @@

          - {{i18n 'groups.logs.from'}}: {{log.prev_value}} + {{i18n 'groups.manage.logs.from'}}: {{log.prev_value}}

          - {{i18n 'groups.logs.to'}}: {{log.new_value}} + {{i18n 'groups.manage.logs.to'}}: {{log.new_value}}

          + + + + +
          + {{yield}} +
          +
          diff --git a/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs b/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs new file mode 100644 index 00000000000..6cd05a616b9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/second-factor-input.hbs @@ -0,0 +1,8 @@ +{{text-field value=value + type="tel" + pattern='[0-9]{6}' + maxlength='6' + id=inputId + autocorrect="off" + autocapitalize="off" + autofocus="autofocus"}} diff --git a/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs b/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs new file mode 100644 index 00000000000..feeed4be439 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/shared-draft-controls.hbs @@ -0,0 +1,24 @@ +
          + {{#if publishing}} + {{i18n "shared_drafts.publishing"}} + {{else}} + {{{i18n "shared_drafts.notice" category=topic.category.name}}} + +
          + + {{category-chooser + value=topic.destination_category_id + onChooseCategory=(action "updateDestinationCategory")}} +
          + +
          + {{#if validCategory}} + {{d-button + action=(action "publish") + label="shared_drafts.publish" + class="btn-primary publish-shared-draft" + icon="clipboard"}} + {{/if}} +
          + {{/if}} +
          diff --git a/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs b/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs index 76127f2bf50..8d0263c71c4 100644 --- a/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs +++ b/app/assets/javascripts/discourse/templates/components/suggested-topics.hbs @@ -1,4 +1,5 @@ -

          {{{suggestedTitle}}}

          +

          {{{suggestedTitle}}}

          +
          {{#if topic.isPrivateMessage}} {{basic-topic-list @@ -9,4 +10,5 @@ {{basic-topic-list topics=topic.suggestedTopics}} {{/if}}
          -

          {{{browseMoreMessage}}}

          + +

          {{{browseMoreMessage}}}

          diff --git a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs deleted file mode 100644 index 8dc8869f6f0..00000000000 --- a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs +++ /dev/null @@ -1,28 +0,0 @@ -{{#if showTagDropdown}} - {{#if tagId}} - {{#if noTagsSelected}} - {{noTagsLabel}} - {{else}} - {{tagId}} - {{/if}} - {{else}} - {{allTagsLabel}} - {{/if}} - - {{#if tags}} - - {{d-icon expandedIcon}} - -
          - - - {{#if renderTags}} - {{#each tags as |t|}} -
          - {{tag-drop-link tagId=t category=currentCategory}} -
          - {{/each}} - {{/if}} -
          - {{/if}} -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/tag-list.hbs b/app/assets/javascripts/discourse/templates/components/tag-list.hbs index a3e912d26ca..7be97e2439f 100644 --- a/app/assets/javascripts/discourse/templates/components/tag-list.hbs +++ b/app/assets/javascripts/discourse/templates/components/tag-list.hbs @@ -4,13 +4,12 @@ {{#if category}} {{category-title-link category=category}} {{/if}} +{{#if tagGroupName}} +

          {{tagGroupName}}

          +{{/if}} {{#each sortedTags as |tag|}}
          - {{#if tag.count}} - {{discourse-tag tag.id}} x {{tag.count}} - {{else}} - {{discourse-tag tag.id}} - {{/if}} + {{discourse-tag tag.id isPrivateMessage=isPrivateMessage tagsForUser=tagsForUser}} {{#if tag.pm_count}}{{d-icon "envelope"}}{{/if}}{{#if tag.totalCount}} x {{tag.totalCount}}{{/if}}
          {{/each}}
          diff --git a/app/assets/javascripts/discourse/templates/components/topic-category.hbs b/app/assets/javascripts/discourse/templates/components/topic-category.hbs index edf11686ba9..122727e3829 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-category.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-category.hbs @@ -1,17 +1,18 @@ -{{#if topic.category.parentCategory}} - {{bound-category-link topic.category.parentCategory}} -{{/if}} -{{bound-category-link topic.category hideParent=true}} +{{#unless topic.isPrivateMessage}} + {{#if topic.category.parentCategory}} + {{bound-category-link topic.category.parentCategory}} + {{/if}} + {{bound-category-link topic.category hideParent=true}} +{{/unless}}
          {{#if siteSettings.tagging_enabled}}
          - {{#each topic.tags as |t|}} - {{discourse-tag t}} - {{/each}} + {{discourse-tags topic mode="list"}}
          {{/if}} {{#if siteSettings.topic_featured_link_enabled}} {{topic-featured-link topic}} {{/if}}
          + {{plugin-outlet name="topic-category" args=(hash topic=topic category=topic.category)}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs index 3af1dba0172..1e6a61eef5c 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-footer-buttons.hbs @@ -50,14 +50,22 @@ disabled=inviteDisabled}} {{/if}} - {{#if topic.isPrivateMessage}} - {{d-button class="standard" + {{#if canArchive}} + {{d-button class="standard archive-topic" title=archiveTitle label=archiveLabel icon=archiveIcon action=toggleArchiveMessage}} {{/if}} + {{#if showEditOnFooter}} + {{d-button class="edit-message" + title="topic.edit_message.help" + label="topic.edit_message.title" + icon="pencil" + action=editFirstPost}} + {{/if}} + {{plugin-outlet name="topic-footer-main-buttons-before-create" args=(hash topic=topic) tagName="" @@ -79,7 +87,9 @@ {{pinned-button pinned=topic.pinned topic=topic}}
          -{{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}} +{{#if showNotificationsButton}} + {{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}} +{{/if}} {{plugin-outlet name="after-topic-footer-buttons" args=(hash topic=topic) diff --git a/app/assets/javascripts/discourse/templates/components/topic-list.hbs b/app/assets/javascripts/discourse/templates/components/topic-list.hbs index b0d2ae5b115..bcdbf0123cd 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-list.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-list.hbs @@ -11,6 +11,7 @@ order=order ascending=ascending sortable=sortable + listTitle=listTitle bulkSelectEnabled=bulkSelectEnabled}} {{/unless}} @@ -40,7 +41,8 @@ expandGloballyPinned=expandGloballyPinned expandAllPinned=expandAllPinned lastVisitedTopic=lastVisitedTopic - selected=selected}} + selected=selected + tagsForUser=tagsForUser}} {{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index 31e9c70b270..59c1891fb1d 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -1,8 +1,7 @@ {{#if visible}}
          -
          - {{bound-avatar avatar "huge"}} + {{bound-avatar user "huge"}} {{#if user.primary_group_name}} {{avatar-flair flairURL=user.primary_group_flair_url @@ -79,12 +78,13 @@
        • {{/if}}
        + {{plugin-outlet name="user-card-additional-controls" args=(hash user=user close=(action "close")) tagName=""}} - {{#if isSuspended}} + {{#if user.suspend_reason}}
        {{d-icon "ban"}} {{i18n 'user.suspended_notice' date=user.suspendedTillDate}}
        @@ -94,16 +94,10 @@ {{#if user.bio_cooked}}
        {{text-overflow class="overflow" text=user.bio_excerpt}}
        {{/if}} {{/if}} - {{#if user.card_badge}} - {{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}} - {{icon-or-image user.card_badge.image title=user.card_badge.name}} - {{/link-to}} - {{/if}} - {{#if hasLocationOrWebsite}}
        {{#if user.location}} - {{d-icon "map-marker"}} {{user.location}} + {{d-icon "map-marker"}} {{user.location}} {{/if}} {{#if user.website_name}} @@ -162,6 +156,7 @@ {{/if}} {{#if showBadges}} + {{#if user.featured_user_badges}}
        {{#each user.featured_user_badges as |ub|}} {{user-badge badge=ub.badge user=user}} @@ -172,6 +167,7 @@ {{/link-to}} {{/if}}
        + {{/if}} {{/if}}
        {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs b/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs index ad7f851e2c7..2158ccb1efc 100644 --- a/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-fields/confirm.hbs @@ -1,3 +1,4 @@ +
        diff --git a/app/assets/javascripts/discourse/templates/components/user-stat.hbs b/app/assets/javascripts/discourse/templates/components/user-stat.hbs index 19c62e09d9d..87b1a220e13 100644 --- a/app/assets/javascripts/discourse/templates/components/user-stat.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-stat.hbs @@ -1,6 +1,8 @@ {{#if isNumber}} {{number value}} + {{else if isDuration}} + {{format-duration value}} {{else}} {{value}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 93864a0863c..204c01ac729 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -9,14 +9,13 @@ {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}} - {{#if model.viewOpen}}
        {{plugin-outlet name="composer-open" args=(hash model=model)}}
        - {{{model.actionTitle}}} + {{composer-action-title model=model canWhisper=canWhisper tabindex=8}} {{#unless site.mobileView}} {{#if whisperOrUnlistTopicText}} @@ -65,7 +64,8 @@
        {{/if}} {{#if canEditTags}} - {{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}} + {{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}} + {{popup-input-tip validation=tagValidation}} {{/if}} @@ -85,6 +85,7 @@ popupMenuOptions=popupMenuOptions draftStatus=model.draftStatus isUploading=isUploading + allowUpload=allowUpload isCancellable=isCancellable uploadProgress=uploadProgress groupsMentioned="groupsMentioned" @@ -103,8 +104,18 @@ icon=model.saveIcon label=model.saveLabel disableSubmit=disableSubmit}} + {{#if site.mobileView}} + + {{#if canEdit}} + {{d-icon "times"}} + {{else}} + {{d-icon "trash-o"}} + {{/if}} + + {{else}} + {{i18n 'cancel'}} + {{/if}} - {{i18n 'cancel'}} {{#if site.mobileView}} {{#if whisperOrUnlistTopic}} @@ -117,8 +128,7 @@ {{#if isUploading}}
        - {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} - {{uploadProgress}}% + {{loading-spinner size="small"}}{{i18n 'upload_selector.uploading'}} {{uploadProgress}}% {{#if isCancellable}} {{d-icon "times"}} {{/if}} @@ -131,7 +141,9 @@
        {{#if site.mobileView}} - {{i18n 'upload'}} + {{#if allowUpload}} + {{d-icon "upload"}} + {{/if}} {{#if showPreview}} {{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 3ae66d589d2..acbb3cdcdb8 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -13,6 +13,17 @@
        {{/if}} +{{#if model.sharedDrafts}} + {{topic-list + class="shared-drafts" + listTitle="shared_drafts.title" + top=top + hideCategory="true" + category=category + topics=model.sharedDrafts + discoveryList=true}} +{{/if}} + {{bulk-select-button selected=selected action="refresh" category=category}} {{#discovery-topics-list model=model refresh="refresh" incomingCount=topicTrackingState.incomingCount}} @@ -25,7 +36,6 @@
        {{count-i18n key="topic_count_" suffix=topicTrackingState.filter count=topicTrackingState.incomingCount}} - {{i18n 'click_to_show'}}
        {{/if}} @@ -66,12 +76,12 @@ {{#footer-message education=footerEducation message=footerMessage}} {{#if latest}} - {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}{{/if}} + {{#if canCreateTopicOnCategory}}{{i18n 'topic.suggest_create_topic'}}.{{/if}} {{else if top}} - {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}} + {{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}}. {{top-period-buttons period=period action="changePeriod"}} {{else}} - {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} + {{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}. {{/if}} {{/footer-message}} diff --git a/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb b/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb index c2ae260f777..b148675b34c 100644 --- a/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb +++ b/app/assets/javascripts/discourse/templates/emoji-picker.raw.hbs.erb @@ -5,7 +5,7 @@ <% JSON.parse(File.read("lib/emoji/groups.json")).each.with_index do |group, group_index| %>
        - +
        <% end %> diff --git a/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs index c36583c6eda..230c236033e 100644 --- a/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs @@ -1,4 +1,4 @@ -
        +
          {{#each options as |option|}}
        • diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 01edd140237..346b3836645 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -1,163 +1,198 @@ {{#d-section pageClass="search" class="search-container"}} - {{scroll-tracker name="full-page-search" tag=searchTerm}} -